From 2c445ccaeb458311ebfe88600aa5b5a0b4d2bf1f Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Mon, 31 Mar 2025 18:04:56 +1100 Subject: [PATCH] wf work + docs --- docs/DOCKER_COMPOSE.md | 39 ++++++++ docs/NOTIFICATIONS.md | 5 +- docs/WORKFLOWS.md | 10 +- front/js/modal.js | 16 ++-- front/php/templates/language/de_de.json | 0 front/php/templates/language/en_us.json | 6 ++ front/php/templates/language/fr_fr.json | 0 front/php/templates/language/uk_ua.json | 0 front/workflowsCore.php | 120 ++++++++++++++++++++++-- server/plugin.py | 1 - server/workflows/conditions.py | 2 +- 11 files changed, 180 insertions(+), 19 deletions(-) mode change 100644 => 100755 front/php/templates/language/de_de.json mode change 100644 => 100755 front/php/templates/language/fr_fr.json mode change 100644 => 100755 front/php/templates/language/uk_ua.json diff --git a/docs/DOCKER_COMPOSE.md b/docs/DOCKER_COMPOSE.md index dcb0b677..89a6d464 100755 --- a/docs/DOCKER_COMPOSE.md +++ b/docs/DOCKER_COMPOSE.md @@ -103,3 +103,42 @@ DEV_LOCATION=/path/to/local/source/code ``` To run the container execute: `sudo docker-compose --env-file /path/to/.env up` + + +## #Example 4: Docker swarm + +Notice how the host network is defined in a swarm setup: + +```yaml +services: + netalertx: + # Use the below line if you want to test the latest dev image + # image: "jokobsk/netalertx-dev:latest" + image: "ghcr.io/jokob-sk/netalertx:latest" + volumes: + - /mnt/MYSERVER/netalertx/config:/config:rw + - /mnt/MYSERVER/netalertx/db:/netalertx/db:rw + - /mnt/MYSERVER/netalertx/logs:/netalertx/front/log:rw + environment: + - TZ=Europe/London + - PORT=20211 + # network_mode: host + networks: + - outside + deploy: + mode: replicated + replicas: 1 + restart_policy: + condition: on-failure + # placement: # ✅ Placement is now correctly inside deploy + # constraints: + # - node.role == manager + # - node.labels.device == NUC2 + +networks: + outside: + external: + name: "host" + + +``` diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md index 60db3f04..291f1f43 100755 --- a/docs/NOTIFICATIONS.md +++ b/docs/NOTIFICATIONS.md @@ -21,6 +21,9 @@ There are 4 settings on the device for influencing notifications. You can: 2. **Alert Down** - Alerts when a device goes down. This setting overrides a disabled **Alert Events** setting, so you will get a notification of a device going down even if you don't have **Alert Events** ticked. Disabling this will disable down and down reconnected notifications on the device. 3. **Skip repeated notifications**, if for example you know there is a temporary issue and want to pause the same notification for this device for a given time. +> [!NOTE] +> Please read through the [NTFPRCS plugin](https://github.com/jokob-sk/NetAlertX/blob/main/front/plugins/notification_processing/README.md) documentation to understand how device and global settings influence the notification processing. + ## Plugin settings 🔌 ![Plugin notification settings](./img/NOTIFICATIONS/Plugin-notification-settings.png) @@ -38,7 +41,7 @@ Click the **Read more in the docs.** Link at the top of each plugin to get more In Notification Processing settings, you can specify blanket rules. These allow you to specify exceptions to the Plugin and Device settings and will override those. -1. Notify on (`NTFPRCS_INCLUDED_SECTIONS`) allows you to specify which events trigger notifications. Usual setups will have `new_devices`, `down_devices`, and possibly `down_reconnected` set. Including `plugin` (dependenton the Plugin `_WATCH` and `_REPORT_ON` settings) and `events` (dependent on the on-device **Alert Events** setting) might be too noisy for most setups. More info in the [NTFPRCS plugin](/front/plugins/notification_processing/README.md) on what events these selections include. +1. Notify on (`NTFPRCS_INCLUDED_SECTIONS`) allows you to specify which events trigger notifications. Usual setups will have `new_devices`, `down_devices`, and possibly `down_reconnected` set. Including `plugin` (dependenton the Plugin `_WATCH` and `_REPORT_ON` settings) and `events` (dependent on the on-device **Alert Events** setting) might be too noisy for most setups. More info in the [NTFPRCS plugin](https://github.com/jokob-sk/NetAlertX/blob/main/front/plugins/notification_processing/README.md) on what events these selections include. 2. Alert down after (`NTFPRCS_alert_down_time`) is useful if you want to wait for some time before the system sends out a down notification for a device. This is related to the on-device **Alert down** setting and only devices with this checked will trigger a down notification. 3. A filter to allow you to set device-specific exceptions to New devices being added to the app. 4. A filter to allow you to set device-specific exceptions to generated Events. diff --git a/docs/WORKFLOWS.md b/docs/WORKFLOWS.md index 2fecf72b..0eb02ac5 100755 --- a/docs/WORKFLOWS.md +++ b/docs/WORKFLOWS.md @@ -54,9 +54,11 @@ Below you can find a couple of configuration examples. ## Example 1: Assign Device to Network Node Based on IP +This workflow assigns newly added devices with IP addresses in the `192.168.1.*` range to the device with the MAC address `6c:6d:6d:6c:6c:6c`. + ### Trigger: - **Object Type**: `Devices` -- **Event Type**: `create` +- **Event Type**: `insert` ### Conditions: - **Logic**: `AND` @@ -71,12 +73,12 @@ Below you can find a couple of configuration examples. - **Field**: `devNetworkNode` - **Value**: `6c:6d:6d:6c:6c:6c` -This workflow assigns newly added devices with IP addresses in the `192.168.1.*` range to the device with the MAC address `6c:6d:6d:6c:6c:6c`. - --- ## Example 2: Mark Device as Not New and Delete If from Google Vendor +This workflow automates the process of marking Google devices as not new and deleting them if they meet the criteria. + ### Trigger: - **Object Type**: `Devices` - **Event Type**: `update` @@ -107,7 +109,7 @@ This workflow assigns newly added devices with IP addresses in the `192.168.1.*` This action deletes the device after it is marked as not new. -This workflow automates the process of marking Google devices as not new and deleting them if they meet the criteria. + --- diff --git a/front/js/modal.js b/front/js/modal.js index d3d6bce5..3b51d711 100755 --- a/front/js/modal.js +++ b/front/js/modal.js @@ -68,11 +68,13 @@ function showModalWarning( callbackFunction = null, triggeredBy = null ) { + prefix = "modal-warning"; + // set captions - $("#modal-warning-title").html(title); - $("#modal-warning-message").html(message); - $("#modal-warning-cancel").html(btnCancel); - $("#modal-warning-OK").html(btnOK); + $(`#${prefix}-title`).html(title); + $(`#${prefix}-message`).html(message); + $(`#${prefix}-cancel`).html(btnCancel); + $(`#${prefix}-OK`).html(btnOK); if (callbackFunction != null) { modalCallbackFunction = callbackFunction; @@ -83,7 +85,7 @@ function showModalWarning( } // Show modal - $("#modal-warning").modal("show"); + $(`#${prefix}`).modal("show"); } // ----------------------------------------------------------------------------- @@ -93,7 +95,8 @@ function showModalInput( btnCancel = getString("Gen_Cancel"), btnOK = getString("Gen_Okay"), callbackFunction = null, - triggeredBy = null + triggeredBy = null, + defaultValue = "" ) { prefix = "modal-input"; @@ -102,6 +105,7 @@ function showModalInput( $(`#${prefix}-message`).html(message); $(`#${prefix}-cancel`).html(btnCancel); $(`#${prefix}-OK`).html(btnOK); + $(`#${prefix}-textarea`).val(defaultValue); if (callbackFunction != null) { modalCallbackFunction = callbackFunction; diff --git a/front/php/templates/language/de_de.json b/front/php/templates/language/de_de.json old mode 100644 new mode 100755 diff --git a/front/php/templates/language/en_us.json b/front/php/templates/language/en_us.json index 92c14b73..9da47e8b 100755 --- a/front/php/templates/language/en_us.json +++ b/front/php/templates/language/en_us.json @@ -681,11 +681,17 @@ "WF_Condition_field": "Field", "WF_Condition_operator": "Operator", "WF_Condition_value": "Value", + "WF_Duplicate": "Duplicate Workflow", + "WF_Import": "Import Workflow", + "WF_Import_Copy": "Paste in the workflow you copied previously.", + "WF_Export": "Export Workflow", + "WF_Export_Copy": "Copy the below workflow and import it where needed.", "WF_Conditions": "Conditions", "WF_Conditions_logic_rules": "Logic rules", "WF_Enabled": "Workflow enabled", "WF_Name": "Workflow name", "WF_Remove": "Remove Workflow", + "WF_Remove_Copy": "Do you want to remove this workflow?", "WF_Save": "Save Workflows", "WF_Trigger": "Trigger", "WF_Trigger_event_type": "Event type", diff --git a/front/php/templates/language/fr_fr.json b/front/php/templates/language/fr_fr.json old mode 100644 new mode 100755 diff --git a/front/php/templates/language/uk_ua.json b/front/php/templates/language/uk_ua.json old mode 100644 new mode 100755 diff --git a/front/workflowsCore.php b/front/workflowsCore.php index 2675fee9..b311d183 100755 --- a/front/workflowsCore.php +++ b/front/workflowsCore.php @@ -16,9 +16,9 @@ -
-
@@ -26,6 +26,11 @@
+
+ +
@@ -45,6 +50,9 @@ let fieldOptions = [ let triggerTypes = [ "Devices" ]; +let triggerEvents = [ + "update", "insert", "delete" +]; let wfEnabledOptions = [ "Yes", "No" @@ -208,7 +216,7 @@ function generateWorkflowUI(wf, wfIndex) { let $eventTypeDropdown = createEditableDropdown( `[${wfIndex}].trigger.event_type`, getString("WF_Trigger_event_type"), - ["update", "create", "delete"], + triggerEvents, wf.trigger.event_type, `wf-${wfIndex}-trigger-event-type` ); @@ -360,7 +368,7 @@ function generateWorkflowUI(wf, wfIndex) { $actionsContainer.append($actionAddButtonWrap) - let $wfRemoveButtonWrap = $("
", { class: "button-container col-sm-12 col-sx-12" }); + let $wfRemoveButtonWrap = $("
", { class: "button-container col-sm-4 col-sx-12" }); let $wfRemoveIcon = $("", { class: "fa-solid fa-trash" @@ -372,10 +380,40 @@ function generateWorkflowUI(wf, wfIndex) { }) .append($wfRemoveIcon) // Add icon .append(` ${getString("WF_Remove")}`); // Add text + + + let $wfDuplicateButtonWrap = $("
", { class: "button-container col-sm-4 col-sx-12" }); + + let $wfDuplicateIcon = $("", { + class: "fa-solid fa-copy" + }); + + let $wfDuplicateButton = $("
", { + class: "pointer duplicate-wf green-hover-text", + wfIndex: wfIndex + }) + .append($wfDuplicateIcon) // Add icon + .append(` ${getString("WF_Duplicate")}`); // Add text + + let $wfExportButtonWrap = $("
", { class: "button-container col-sm-4 col-sx-12" }); + + let $wfExportIcon = $("", { + class: "fa-solid fa-file-export" + }); + + let $wfExportButton = $("
", { + class: "pointer export-wf green-hover-text", + wfIndex: wfIndex + }) + .append($wfExportIcon) // Add icon + .append(` ${getString("WF_Export")}`); // Add text $wfCollapsiblePanel.append($actionsContainer); + $wfCollapsiblePanel.append($wfDuplicateButtonWrap.append($wfDuplicateButton)) + $wfCollapsiblePanel.append($wfExportButtonWrap.append($wfExportButton)) $wfCollapsiblePanel.append($wfRemoveButtonWrap.append($wfRemoveButton)) + $wfContainer.append($wfCollapsiblePanel) @@ -765,7 +803,62 @@ function addWorkflow(workflows) { // Function to remove a Workflow function removeWorkflow(workflows, wfIndex) { - workflows.splice(wfIndex, 1); + showModalWarning ('', '', + '', '', `executeRemoveWorkflow`, wfIndex); +} + +// --------------------------------------------------- +// Function to execute the remove of a Workflow +function executeRemoveWorkflow() { + + workflows = getWorkflowsJson() + + workflows.splice($('#modal-warning').attr("data-myparam-triggered-by"), 1); + + updateWorkflowsJson(workflows) + + // Re-render the UI + renderWorkflows(); +} + +// --------------------------------------------------- +// Function to duplicate a Workflow +function duplicateWorkflow(workflows, wfIndex) { + + workflows.push(workflows[wfIndex]) + + updateWorkflowsJson(workflows) + + // Re-render the UI + renderWorkflows(); +} + +// --------------------------------------------------- +// Function to export a Workflow +function exportWorkflow(workflows, wfIndex) { + +// Add new icon as base64 string +showModalInput (' ', '', + '', '', null, null, JSON.stringify(workflows[wfIndex], null, 2)); +} + +// --------------------------------------------------- +// Function to import a Workflow +function importWorkflow(workflows, wfIndex) { + +// Add new icon as base64 string +showModalInput (' ', '', + '', '', 'importWorkflowExecute', null, "" ); + +} + +function importWorkflowExecute() +{ + var json = JSON.parse($('#modal-input-textarea').val()); + + workflows = getWorkflowsJson() + + workflows.push(json); updateWorkflowsJson(workflows) @@ -1019,6 +1112,21 @@ $(document).on("click", ".remove-wf", function () { removeWorkflow(getWorkflowsJson(), wfIndex); }); +$(document).on("click", ".duplicate-wf", function () { + let wfIndex = $(this).attr("wfindex"); + duplicateWorkflow(getWorkflowsJson(), wfIndex); +}); + +$(document).on("click", ".export-wf", function () { + let wfIndex = $(this).attr("wfindex"); + exportWorkflow(getWorkflowsJson(), wfIndex); +}); + +$(document).on("click", ".import-wf", function () { + let wfIndex = $(this).attr("wfindex"); + importWorkflow(getWorkflowsJson(), wfIndex); +}); + $(document).on("click", ".add-condition", function () { let wfIndex = $(this).attr("wfindex"); let parentIndexPath = $(this).attr("parentIndexPath"); diff --git a/server/plugin.py b/server/plugin.py index 4bf3b361..cbf07100 100755 --- a/server/plugin.py +++ b/server/plugin.py @@ -819,7 +819,6 @@ class plugin_object_class: # Check if self.status is valid if self.status not in ["exists", "watched-changed", "watched-not-changed", "new", "not-processed", "missing-in-last-scan"]: - mylog('none', [f'[plugin_object_class] ERROR on objDbRow: {objDbRow}']) raise ValueError(f"Invalid status value for plugin object ({self.pluginPref}|{self.primaryId}|{self.watched1}) invalid status: {self.status} on objDbRow:", objDbRow) self.idsHash = str(hash(str(self.primaryId) + str(self.secondaryId))) diff --git a/server/workflows/conditions.py b/server/workflows/conditions.py index 801974df..c8ea4229 100755 --- a/server/workflows/conditions.py +++ b/server/workflows/conditions.py @@ -41,7 +41,7 @@ class Condition: if self.operator == "equals": result = str(obj_value) == str(self.value) elif self.operator == "contains": - result = str(self.value) in str(obj_value) + result = str(self.value).lower() in str(obj_value).lower() elif self.operator == "regex": result = bool(re.match(self.value, str(obj_value))) else: