mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-09-20 12:49:00 -04:00
FE+DOCS: Wildcards in custom props #1773
This commit is contained in:
1 parent
939e6494ba
commit
cddb237cd3
7 files changed
+158
-51
No files matched your search
@@ -117,7 +117,7 @@ Slowness can be caused by:
|
||||
|
||||
With `ARPSCAN` scans some devices might flip IP addresses after each scan triggering false notifications. This is because some devices respond to broadcast calls and thus different IPs after scans are logged.
|
||||
|
||||
See how to prevent IP flipping in the [ARPSCAN plugin guide](/server/plugins/arp_scan/README.md).
|
||||
See how to prevent IP flipping in the [ARPSCAN plugin guide](https://github.com/netalertx/NetAlertX/blob/main/server/plugins/arp_scan/README.md).
|
||||
|
||||
Alternatively adjust your [notification settings](./NOTIFICATIONS.md) to prevent false positives by filtering out events or devices.
|
||||
|
||||
|
||||
@@ -66,13 +66,30 @@ Custom properties are structured as a list of objects, where each property inclu
|
||||
|--------------------|-----------------------------------------------------------------------------|
|
||||
| `CUSTPROP_icon` | The icon (Base64-encoded HTML) displayed for the property. |
|
||||
| `CUSTPROP_type` | The action type (e.g., `show_notes`, `link`, `delete_dev`). |
|
||||
| `CUSTPROP_name` | A short name or title for the property. |
|
||||
| `CUSTPROP_args` | Arguments for the action (e.g., URL or modal text). |
|
||||
| `CUSTPROP_name` | A short name or title for the property. Supports `{{fieldName}}` wildcards. |
|
||||
| `CUSTPROP_args` | Arguments for the action (e.g., URL or modal text). Supports `{{fieldName}}` wildcards. |
|
||||
| `CUSTPROP_notes` | Additional notes or details displayed when applicable. |
|
||||
| `CUSTPROP_show` | A boolean to control visibility (`true` to show on the listing page). |
|
||||
|
||||
---
|
||||
|
||||
## Wildcards in `CUSTPROP_name` / `CUSTPROP_args`
|
||||
|
||||
`CUSTPROP_name` and `CUSTPROP_args` are resolved per-device before rendering, so you can reference any of that device's own fields with `{{fieldName}}` - field names are matched case-insensitively, so `{{devLastIp}}` and `{{devLastIP}}` are equivalent. If a field name doesn't exist, the placeholder is left as-is (e.g. `{{devTypo}}` stays visible) rather than silently disappearing, to make a typo obvious while you're setting one up.
|
||||
|
||||
This is what makes a single `link`/`link_new_tab` custom property work across every device rather than one URL per device - e.g. to jump to a device's traffic log in an AdGuard Home instance, filtered to that device's IP:
|
||||
|
||||
```
|
||||
CUSTPROP_type: link_new_tab
|
||||
CUSTPROP_args: http://my_adguard_url/#logs?search={{devLastIP}}
|
||||
```
|
||||
|
||||
Because this is set once (either directly on a device, or as a default applied to every new device via the `NEWDEV_devCustomProps` setting), it applies across your whole device list, not just one device at a time.
|
||||
|
||||
Commonly useful fields: `devMac`, `devLastIP`, `devName`, `devVendor`, `devType`, `devGUID`.
|
||||
|
||||
---
|
||||
|
||||
## Available Action Types
|
||||
|
||||
- **Show Notes**: Displays a modal with a title and additional notes.
|
||||
|
||||
@@ -25,7 +25,7 @@ Get **NetAlertX** up and running in a few simple steps.
|
||||
|
||||
> [!NOTE]
|
||||
> Configure your SMTP settings or enable additional `▶️ publisher` plugins to send alerts.
|
||||
> For more flexibility, try [📚 `_publisher_apprise`](/server/plugins/_publisher_apprise/), which supports over 80 notification services.
|
||||
> For more flexibility, try [📚 `_publisher_apprise`](https://github.com/netalertx/NetAlertX/tree/main/server/plugins/_publisher_apprise/), which supports over 80 notification services.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -39,14 +39,14 @@ Two plugins help maintain the system’s performance:
|
||||
### **1. Database Cleanup (DBCLNP)**
|
||||
|
||||
* Handles database maintenance and cleanup.
|
||||
* See the [DB Cleanup Plugin Docs](/server/plugins/db_cleanup/README.md).
|
||||
* See the [DB Cleanup Plugin Docs](https://github.com/netalertx/NetAlertX/tree/main/server/plugins/db_cleanup/README.md).
|
||||
* Ensure it’s not failing by checking logs.
|
||||
* Adjust the schedule (`DBCLNP_RUN_SCHD`) and timeout (`DBCLNP_RUN_TIMEOUT`) if necessary.
|
||||
|
||||
### **2. Maintenance (MAINT)**
|
||||
|
||||
* Cleans logs and performs general maintenance tasks.
|
||||
* See the [Maintenance Plugin Docs](/server/plugins/maintenance/README.md).
|
||||
* See the [Maintenance Plugin Docs](https://github.com/netalertx/NetAlertX/tree/main/server/plugins/maintenance/README.md).
|
||||
* Verify proper operation via logs.
|
||||
* Adjust the schedule (`MAINT_RUN_SCHD`) and timeout (`MAINT_RUN_TIMEOUT`) if needed.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Plugins communicate with NetAlertX by writing results to a **pipe-delimited log
|
||||
|
||||
## Using `plugin_helper.py`
|
||||
|
||||
The easiest way to ensure correct output is to use the [`plugin_helper.py`](../server/plugins/plugin_helper.py) library:
|
||||
The easiest way to ensure correct output is to use the [`plugin_helper.py`](https://github.com/netalertx/NetAlertX/blob/main/server/plugins/plugin_helper.py) library:
|
||||
|
||||
```python
|
||||
from plugin_helper import Plugin_Objects
|
||||
|
||||
+97
-44
@@ -586,6 +586,18 @@ function badgeFromRowData(rowData) {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Build a plain {fieldName: value} device object from a DataTables positional
|
||||
// row - for callers (e.g. renderCustomProps' {{fieldName}} wildcards) that
|
||||
// need to look up an arbitrary device field by name, not just a fixed few.
|
||||
function deviceObjectFromRowData(rowData) {
|
||||
const device = {};
|
||||
DEVICE_COLUMN_FIELDS.forEach((field, oldIndex) => {
|
||||
device[field] = rowData[mapIndx(oldIndex)];
|
||||
});
|
||||
return device;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Build the rich empty-table onboarding message (HTML).
|
||||
// Used as the DataTables 'emptyTable' language option.
|
||||
@@ -1005,7 +1017,7 @@ function initializeDatatable (status) {
|
||||
{targets: [mapIndx(COL.devCustomProps)],
|
||||
'createdCell': function (td, cellData, rowData, row, col) {
|
||||
if (!emptyArr.includes(cellData)){
|
||||
$(td).html (`<span>${renderCustomProps(cellData, rowData[mapIndx(COL.devMac)])}</span>`);
|
||||
$(td).html (`<span>${renderCustomProps(cellData, deviceObjectFromRowData(rowData))}</span>`);
|
||||
} else {
|
||||
$(td).html ('');
|
||||
}
|
||||
@@ -1214,57 +1226,98 @@ function getMacsOfShownDevices() {
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Handle custom actions/properties on a device
|
||||
function renderCustomProps(custProps, mac) {
|
||||
// Decode and parse the custom properties
|
||||
// Handle custom actions/properties on a device.
|
||||
//
|
||||
// CUSTPROP_name and CUSTPROP_args support {{fieldName}} wildcards (e.g.
|
||||
// {{devLastIP}}) resolved against this row's own fields - see GH #1773. The
|
||||
// resulting action is dispatched via a single delegated click handler reading
|
||||
// data-* attributes (see below) rather than an inline onclick="..." string,
|
||||
// so a device-controlled value (a DHCP hostname containing a quote, say)
|
||||
// can't break out of inline JS the way it could with string-built onclick
|
||||
// handlers.
|
||||
function renderCustomProps(custProps, device) {
|
||||
const mac = device.devMac;
|
||||
|
||||
if (!isBase64(custProps)) {
|
||||
|
||||
console.error(`Unable to decode CustomProps for ${mac}`);
|
||||
console.error(custProps);
|
||||
|
||||
} else{
|
||||
const props = JSON.parse(atob(custProps));
|
||||
let html = "";
|
||||
|
||||
props.forEach((propGroup, index) => {
|
||||
const propMap = Object.fromEntries(
|
||||
propGroup.map(prop => Object.entries(prop)[0]) // Convert array of objects to key-value pairs
|
||||
);
|
||||
|
||||
if (propMap["CUSTPROP_show"] === true) { // Render if visible
|
||||
let onClickEvent = "";
|
||||
|
||||
switch (propMap["CUSTPROP_type"]) {
|
||||
case "show_notes":
|
||||
onClickEvent = `showModalOK('${propMap["CUSTPROP_name"]}','${propMap["CUSTPROP_notes"]}')`;
|
||||
break;
|
||||
case "link":
|
||||
onClickEvent = `window.location.href='${propMap["CUSTPROP_args"]}';`;
|
||||
break;
|
||||
case "link_new_tab":
|
||||
onClickEvent = `openInNewTab('${propMap["CUSTPROP_args"]}')`;
|
||||
break;
|
||||
case "run_plugin":
|
||||
onClickEvent = `alert('Not implemented')`;
|
||||
break;
|
||||
case "delete_dev":
|
||||
onClickEvent = `askDeleteDeviceByMac('${mac}')`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
html += `<div class="pointer devicePropAction" onclick="${onClickEvent}" title="${propMap["CUSTPROP_name"]} ${propMap["CUSTPROP_args"]}"> ${atob(propMap["CUSTPROP_icon"])} </div>`;
|
||||
}
|
||||
});
|
||||
|
||||
return html;
|
||||
return "Error, check browser Console log";
|
||||
}
|
||||
|
||||
return "Error, check browser Console log"
|
||||
const props = JSON.parse(atob(custProps));
|
||||
let html = "";
|
||||
|
||||
props.forEach((propGroup) => {
|
||||
const propMap = Object.fromEntries(
|
||||
propGroup.map(prop => Object.entries(prop)[0]) // Convert array of objects to key-value pairs
|
||||
);
|
||||
|
||||
if (propMap["CUSTPROP_show"] !== true) {
|
||||
return; // Not visible
|
||||
}
|
||||
|
||||
const type = propMap["CUSTPROP_type"];
|
||||
const isUrlType = (type === "link" || type === "link_new_tab");
|
||||
|
||||
// Plain (unescaped) resolved values, for the human-readable tooltip.
|
||||
const namePlain = resolveDeviceWildcards(propMap["CUSTPROP_name"], device);
|
||||
const argsPlain = resolveDeviceWildcards(propMap["CUSTPROP_args"], device);
|
||||
|
||||
// The value actually used for navigation: URL-encode substituted fields
|
||||
// when this prop's args is a URL, so e.g. a device name with a space or
|
||||
// "&" in it can't corrupt the query string.
|
||||
const argsForAction = isUrlType
|
||||
? resolveDeviceWildcards(propMap["CUSTPROP_args"], device, encodeURIComponent)
|
||||
: argsPlain;
|
||||
|
||||
const notesPlain = propMap["CUSTPROP_notes"] || "";
|
||||
|
||||
html += `<div class="pointer devicePropAction"
|
||||
data-action="${encodeSpecialChars(type)}"
|
||||
data-args="${encodeSpecialChars(argsForAction)}"
|
||||
data-name="${encodeSpecialChars(namePlain)}"
|
||||
data-notes="${encodeSpecialChars(notesPlain)}"
|
||||
data-mac="${encodeSpecialChars(mac)}"
|
||||
title="${encodeSpecialChars(`${namePlain} ${argsPlain}`)}">
|
||||
${atob(propMap["CUSTPROP_icon"])}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
// Single delegated handler for every custom-property action rendered by
|
||||
// renderCustomProps() above - bound once, so it keeps working after
|
||||
// DataTables redraws without needing to be re-attached per row.
|
||||
$(document).on('click', '.devicePropAction', function () {
|
||||
const el = $(this);
|
||||
const action = el.data('action');
|
||||
const args = el.data('args') ?? '';
|
||||
const name = el.data('name') ?? '';
|
||||
const notes = el.data('notes') ?? '';
|
||||
const mac = el.data('mac') ?? '';
|
||||
|
||||
switch (action) {
|
||||
case "show_notes":
|
||||
showModalOK(name, notes);
|
||||
break;
|
||||
case "link":
|
||||
window.location.href = args;
|
||||
break;
|
||||
case "link_new_tab":
|
||||
openInNewTab(args);
|
||||
break;
|
||||
case "run_plugin":
|
||||
alert('Not implemented');
|
||||
break;
|
||||
case "delete_dev":
|
||||
askDeleteDeviceByMac(mac);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -1381,6 +1381,43 @@ function getDevicesList()
|
||||
return devicesList;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Resolve {{fieldName}} wildcards in a template string against a device object
|
||||
// (case-insensitive field names, so {{devLastIp}} matches devLastIP). Unknown
|
||||
// fields are left as the literal "{{fieldName}}" placeholder so a typo while
|
||||
// configuring e.g. a custom property stays visible instead of silently
|
||||
// disappearing. escapeFn is applied to each substituted VALUE only (not the
|
||||
// surrounding template text) - pass encodeURIComponent when the template is a
|
||||
// URL, or leave the default identity function for plain text the caller will
|
||||
// HTML-escape itself afterwards.
|
||||
function resolveDeviceWildcards(str, device, escapeFn = (v) => v) {
|
||||
if (!str || typeof str !== 'string' || str.indexOf('{{') === -1 || !device) {
|
||||
return str;
|
||||
}
|
||||
|
||||
const lowerCaseFields = {};
|
||||
for (const key in device) {
|
||||
lowerCaseFields[key.toLowerCase()] = device[key];
|
||||
}
|
||||
|
||||
return str.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, fieldName) => {
|
||||
const lowerFieldName = fieldName.toLowerCase();
|
||||
if (!(lowerFieldName in lowerCaseFields)) {
|
||||
return match; // unknown field - leave the placeholder visible
|
||||
}
|
||||
const value = lowerCaseFields[lowerFieldName];
|
||||
return escapeFn(value === null || value === undefined ? '' : String(value));
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Convenience wrapper for callers that only have a MAC, not a full device
|
||||
// object already in scope - looks the device up from the cached device list.
|
||||
function resolveDeviceWildcardsByMac(str, mac, escapeFn = (v) => v) {
|
||||
const device = getDevicesList().find(d => d.devMac === mac);
|
||||
return device ? resolveDeviceWildcards(str, device, escapeFn) : str;
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// apply theme
|
||||
|
||||
Reference in new issue
Block a user