From 2658d5703e7ac2cf2310bea337d244a05dc1afe6 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 9 Jul 2026 13:34:54 +1000 Subject: [PATCH 1/2] BE+FE: less logging bloat + better notification preview + skeletons --- front/css/app.css | 61 +++++++++++++++---- .../unifi_api_import/unifi_api_import.py | 4 +- front/report_templates/report_template.html | 1 + server/messaging/reporting.py | 4 +- server/models/device_history_instance.py | 6 -- server/models/notification_instance.py | 47 ++++++++++++-- server/workflows/conditions.py | 4 +- server/workflows/manager.py | 10 +-- server/workflows/triggers.py | 6 +- test/backend/test_notification_templates.py | 23 +++---- 10 files changed, 115 insertions(+), 51 deletions(-) diff --git a/front/css/app.css b/front/css/app.css index facd9d994..6f3a2d0c7 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 678e8a466..5e3653585 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 58c600bd1..cbd301528 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 52cbf4751..ea9f9a291 100755 --- a/server/messaging/reporting.py +++ b/server/messaging/reporting.py @@ -221,8 +221,8 @@ def get_section_condition(section): 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 9547ffff8..01f84bfa5 100644 --- a/server/models/device_history_instance.py +++ b/server/models/device_history_instance.py @@ -206,12 +206,6 @@ def _query_grouped(self, devGUID, changedColumn, changedBy, limit, offset, sort, 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 38095093f..007e0656c 100755 --- a/server/models/notification_instance.py +++ b/server/models/notification_instance.py @@ -126,9 +126,14 @@ def create(self, JSON, Extra=""): 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 @@ def create(self, JSON, Extra=""): 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 @@ def save(self): # ----------------------------------------------------------------------------- # 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 d68b8c481..db6d1299c 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 @@ def evaluate(self, event): 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 e171866fd..df094c1a2 100755 --- a/server/workflows/manager.py +++ b/server/workflows/manager.py @@ -28,7 +28,6 @@ def __init__(self, db): # ------------------------------------------------------------------------- # 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 @@ def _scan(node): # ------------------------------------------------------------------------- # Loading - def load_workflows(self): """Load workflows from workflows.json, rejecting any with invalid tokens.""" try: @@ -87,7 +85,6 @@ def get_new_app_events(self): # ------------------------------------------------------------------------- # Event processing - def process_event(self, event): """Process one AppEvent against all enabled workflows.""" evGuid = event["guid"] @@ -116,7 +113,6 @@ def process_event(self, event): # ------------------------------------------------------------------------- # 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 @@ def execute_workflow(self, workflow, trigger): 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 @@ def execute_actions(self, actions, trigger): 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 86e305ec3..fb365ad42 100755 --- a/server/workflows/triggers.py +++ b/server/workflows/triggers.py @@ -24,7 +24,7 @@ def __init__(self, triggerJson, event, db): 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 @@ def __init__(self, triggerJson, event, db): 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 3a17237f2..1e8b8d9a1 100644 --- a/test/backend/test_notification_templates.py +++ b/test/backend/test_notification_templates.py @@ -74,9 +74,10 @@ def test_empty_section_returns_empty(self, mock_setting): 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 @@ def test_legacy_fallback_no_template(self, mock_setting): 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 @@ def test_template_substitution(self, mock_setting): 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 @@ def test_missing_field_safe_failure(self, mock_setting): 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 @@ def test_section_headers_disabled(self, mock_setting): 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 @@ def test_section_headers_default_enabled(self, mock_setting): 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 @@ def test_mixed_valid_and_invalid_fields(self, mock_setting): 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 @@ def test_down_devices_template(self, mock_setting): 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 @@ def test_down_reconnected_template(self, mock_setting): 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 @@ def test_html_unchanged_with_template(self, mock_setting): 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) From 11b27b1a0f11d363b5a0f15d96dad28a7a999c1b Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Thu, 9 Jul 2026 13:51:09 +1000 Subject: [PATCH 2/2] BE: better notification preview --- front/report_templates/report_template.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/report_templates/report_template.html b/front/report_templates/report_template.html index cbd301528..39c6134ad 100755 --- a/front/report_templates/report_template.html +++ b/front/report_templates/report_template.html @@ -1,6 +1,7 @@ + PREHEADER @@ -10,8 +11,7 @@ -
- PREHEADER + NEW_DEVICES_TABLE DOWN_DEVICES_TABLE DOWN_RECONNECTED_TABLE