diff --git a/back/device_heuristics_rules.json b/back/device_heuristics_rules.json
index 419c3da1..783ca333 100755
--- a/back/device_heuristics_rules.json
+++ b/back/device_heuristics_rules.json
@@ -5,7 +5,64 @@
"matching_pattern": [
{ "mac_prefix": "INTERNET", "vendor": "" }
],
- "name_pattern": []
+ "name_pattern": [],
+ "ip_pattern": [
+ "^192\\.168\\.1\\.1$",
+ "^192\\.168\\.0\\.1$",
+ "^10\\.0\\.0\\.1$"
+ ]
+ },
+ {
+ "dev_type": "Smart Switch",
+ "icon_html": "",
+ "matching_pattern": [
+ { "mac_prefix": "003192", "vendor": "TP-Link" },
+ { "mac_prefix": "50C7BF", "vendor": "TP-Link" },
+ { "mac_prefix": "B04E26", "vendor": "TP-Link" }
+ ],
+ "name_pattern": ["hs200", "hs210", "hs220", "ks230", "smart switch", "light switch", "wall switch"]
+ },
+ {
+ "dev_type": "Smart Plug",
+ "icon_html": "",
+ "matching_pattern": [
+ { "mac_prefix": "2887BA", "vendor": "TP-Link" }
+ ],
+ "name_pattern": ["kp115", "hs100", "hs103", "hs105", "smart plug", "outlet", "plug"]
+ },
+ {
+ "dev_type": "Smart Speaker",
+ "icon_html": "",
+ "matching_pattern": [
+ { "mac_prefix": "14C14E", "vendor": "Google" },
+ { "mac_prefix": "44650D", "vendor": "Amazon" },
+ { "mac_prefix": "74ACB9", "vendor": "Google" }
+ ],
+ "name_pattern": ["echo", "alexa", "dot", "nest-audio", "nest-mini", "google-home"]
+ },
+ {
+ "dev_type": "Smart Appliance",
+ "icon_html": "",
+ "matching_pattern": [
+ { "mac_prefix": "446FF8", "vendor": "Dyson" }
+ ],
+ "name_pattern": ["dyson", "purifier", "humidifier", "fan"]
+ },
+ {
+ "dev_type": "Smart Home",
+ "icon_html": "",
+ "matching_pattern": [],
+ "name_pattern": ["google", "chromecast", "nest", "hub"]
+ },
+ {
+ "dev_type": "Phone",
+ "icon_html": "",
+ "matching_pattern": [
+ { "mac_prefix": "001A79", "vendor": "Apple" },
+ { "mac_prefix": "B0BE83", "vendor": "Samsung" },
+ { "mac_prefix": "BC926B", "vendor": "Motorola" }
+ ],
+ "name_pattern": ["iphone", "ipad", "pixel", "galaxy", "redmi", "android", "samsung"]
},
{
"dev_type": "Access Point",
@@ -16,24 +73,7 @@
{ "mac_prefix": "F4F5D8", "vendor": "TP-Link" },
{ "mac_prefix": "F88E85", "vendor": "Netgear" }
],
- "name_pattern": ["router", "gateway", "ap", "access point", "access-point", "switch"]
- },
- {
- "dev_type": "Phone",
- "icon_html": "",
- "matching_pattern": [
- { "mac_prefix": "001A79", "vendor": "Apple" },
- { "mac_prefix": "B0BE83", "vendor": "Samsung" },
- { "mac_prefix": "BC926B", "vendor": "Motorola" }
- ],
- "name_pattern": ["iphone", "ipad", "pixel", "galaxy", "redmi"]
- },
- {
- "dev_type": "Phone",
- "icon_html": "",
- "matching_pattern": [
- ],
- "name_pattern": ["android","samsung"]
+ "name_pattern": ["router", "gateway", "ap", "access point", "access-point", "switch", "sg105", "sg108", "managed switch", "unmanaged switch", "poe switch", "ethernet switch"]
},
{
"dev_type": "Tablet",
@@ -43,25 +83,19 @@
{ "mac_prefix": "BC4C4C", "vendor": "Samsung" }
],
"name_pattern": ["tablet", "pad"]
- },
- {
- "dev_type": "IoT",
- "icon_html": "",
- "matching_pattern": [
- { "mac_prefix": "B827EB", "vendor": "Raspberry Pi" },
- { "mac_prefix": "DCA632", "vendor": "Raspberry Pi" }
- ],
- "name_pattern": ["raspberry", "pi"]
},
{
"dev_type": "IoT",
"icon_html": "",
"matching_pattern": [
+ { "mac_prefix": "B827EB", "vendor": "Raspberry Pi" },
+ { "mac_prefix": "DCA632", "vendor": "Raspberry Pi" },
{ "mac_prefix": "840D8E", "vendor": "Espressif" },
{ "mac_prefix": "ECFABC", "vendor": "Espressif" },
- { "mac_prefix": "7C9EBD", "vendor": "Espressif" }
+ { "mac_prefix": "7C9EBD", "vendor": "Espressif" },
+ { "mac_prefix": "286DCD", "vendor": "Beijing Winner Microelectronics" }
],
- "name_pattern": ["raspberry", "pi"]
+ "name_pattern": ["raspberry", "pi", "thingsturn", "w600", "w601"]
},
{
"dev_type": "Desktop",
@@ -69,9 +103,11 @@
"matching_pattern": [
{ "mac_prefix": "001422", "vendor": "Dell" },
{ "mac_prefix": "001874", "vendor": "Lenovo" },
- { "mac_prefix": "00E04C", "vendor": "Hewlett Packard" }
+ { "mac_prefix": "00E04C", "vendor": "Hewlett Packard" },
+ { "mac_prefix": "F44D30", "vendor": "Elitegroup Computer Systems" },
+ { "mac_prefix": "1C697A", "vendor": "Elitegroup Computer Systems" }
],
- "name_pattern": ["desktop", "pc", "computer"]
+ "name_pattern": ["desktop", "pc", "computer", "liva", "ecs"]
},
{
"dev_type": "Laptop",
@@ -80,9 +116,10 @@
{ "mac_prefix": "3C0754", "vendor": "HP" },
{ "mac_prefix": "0017A4", "vendor": "Dell" },
{ "mac_prefix": "F4CE46", "vendor": "Lenovo" },
- { "mac_prefix": "409F38", "vendor": "Acer" }
+ { "mac_prefix": "409F38", "vendor": "Acer" },
+ { "mac_prefix": "9CB6D0", "vendor": "Rivet Networks" }
],
- "name_pattern": ["macbook", "imac", "laptop", "notebook"]
+ "name_pattern": ["macbook", "imac", "laptop", "notebook", "alienware", "razer", "msi"]
},
{
"dev_type": "Server",
@@ -123,9 +160,10 @@
"matching_pattern": [
{ "mac_prefix": "001FA7", "vendor": "Sony" },
{ "mac_prefix": "7C04D0", "vendor": "Nintendo" },
- { "mac_prefix": "EC26CA", "vendor": "Sony" }
+ { "mac_prefix": "EC26CA", "vendor": "Sony" },
+ { "mac_prefix": "48B02D", "vendor": "NVIDIA" }
],
- "name_pattern": ["playstation", "xbox"]
+ "name_pattern": ["playstation", "xbox", "shield", "nvidia"]
},
{
"dev_type": "Camera",
@@ -138,15 +176,6 @@
],
"name_pattern": ["camera", "cam", "webcam"]
},
- {
- "dev_type": "Smart Speaker",
- "icon_html": "",
- "matching_pattern": [
- { "mac_prefix": "44650D", "vendor": "Amazon" },
- { "mac_prefix": "74ACB9", "vendor": "Google" }
- ],
- "name_pattern": ["echo", "alexa", "dot"]
- },
{
"dev_type": "Router",
"icon_html": "",
@@ -154,23 +183,13 @@
{ "mac_prefix": "000C29", "vendor": "Cisco" },
{ "mac_prefix": "00155D", "vendor": "MikroTik" }
],
- "name_pattern": ["router", "gateway", "ap", "access point", "access-point"],
- "ip_pattern": [
- "^192\\.168\\.[0-1]\\.1$",
- "^10\\.0\\.0\\.1$"
- ]
+ "name_pattern": ["router", "gateway", "ap", "access point"]
},
{
"dev_type": "Smart Light",
"icon_html": "",
"matching_pattern": [],
- "name_pattern": ["hue", "lifx", "bulb"]
- },
- {
- "dev_type": "Smart Home",
- "icon_html": "",
- "matching_pattern": [],
- "name_pattern": ["google", "chromecast", "nest"]
+ "name_pattern": ["hue", "lifx", "bulb", "light"]
},
{
"dev_type": "Smartwatch",
@@ -187,14 +206,9 @@
{
"dev_type": "Security Device",
"icon_html": "",
- "matching_pattern": [],
- "name_pattern": ["doorbell", "lock", "security"]
- },
- {
- "dev_type": "Smart Light",
- "icon_html": "",
"matching_pattern": [
+ { "mac_prefix": "047BCB", "vendor": "Universal Global Scientific" }
],
- "name_pattern": ["light","bulb"]
+ "name_pattern": ["doorbell", "lock", "security", "mmd-", "ring"]
}
-]
+]
\ No newline at end of file
diff --git a/docs/API_DEVICE_FIELD_LOCK.md b/docs/API_DEVICE_FIELD_LOCK.md
index 5819301e..61180cf7 100644
--- a/docs/API_DEVICE_FIELD_LOCK.md
+++ b/docs/API_DEVICE_FIELD_LOCK.md
@@ -138,36 +138,6 @@ The Device Edit form displays lock/unlock buttons for all tracked fields:
2. **Unlock Button** (🔓): Click to allow plugin overwrites again
3. **Source Indicator**: Shows current field source (USER, LOCKED, NEWDEV, or plugin name)
-## UI Workflow
-
-### Locking a Field via UI
-
-1. Navigate to Device Details
-2. Find the field you want to protect
-3. Click the lock button (🔒) next to the field
-4. Button changes to unlock (🔓) and source indicator turns red (LOCKED)
-5. Field is now protected from plugin overwrites
-
-### Unlocking a Field via UI
-
-1. Find the locked field (button shows 🔓)
-2. Click the unlock button
-3. Button changes back to lock (🔒) and source resets to NEWDEV
-4. Plugins can now update this field again
-
-## Authorization
-
-All lock/unlock operations require:
-- Valid API token in `Authorization: Bearer {token}` header
-- User must be authenticated to the NetAlertX instance
-
-## Implementation Details
-
-### Backend Logic
-The lock/unlock feature is implemented in:
-- **API Endpoint**: `/server/api_server/api_server_start.py` - `api_device_field_lock()`
-- **Data Model**: `/server/models/device_instance.py` - Authorization checks in `setDeviceData()`
-- **Database**: Devices table with `*Source` columns tracking field origins
### Authorization Handler
@@ -179,6 +149,9 @@ The authoritative field update logic prevents plugin overwrites:
4. If source is `NEWDEV` or plugin name, plugin update is accepted
## See Also
+
+- [Device locking](./DEVICE_FIELD_LOCK.md)
+- [Device source fields](./DEVICE_SOURCE_FIELDS.md)
- [API Device Endpoints Documentation](./API_DEVICE.md)
- [Authoritative Field Updates System](./PLUGINS_DEV.md#authoritative-fields)
- [Plugin Configuration Reference](./PLUGINS_DEV_CONFIG.md)
diff --git a/docs/DEVICE_FIELD_LOCK.md b/docs/DEVICE_FIELD_LOCK.md
index fcd24960..47369c57 100644
--- a/docs/DEVICE_FIELD_LOCK.md
+++ b/docs/DEVICE_FIELD_LOCK.md
@@ -37,7 +37,12 @@ Each locked field has a "source" indicator that shows you why the value is prote
| 📡 **NEWDEV** | Default/unset value | Yes, plugins can update |
| 📡 **Plugin name** | Last updated by a plugin (e.g., UNIFIAPI) | Yes, plugins can update if field in SET_ALWAYS |
-## How to Use
+Overwrite rules are
+
+> [!TIP]
+> You can bulk-unlock devices in the [Multi-edit](./DEVICES_BULK_EDITING.md) dialog. This removes all `USER` and `LOCKED` values from all `*Source` fields of selected devices.
+
+## Usage Examples
### Lock a Field (Prevent Plugin Changes)
@@ -147,13 +152,13 @@ Each locked field has a "source" indicator that shows you why the value is prote
- Check if you accidentally unlocked it
- Open an issue if it persists
-## For More Information
+## See also
-- **Technical details:** See [API_DEVICE_FIELD_LOCK.md](API_DEVICE_FIELD_LOCK.md)
-- **Plugin configuration:** See [PLUGINS_DEV_CONFIG.md](PLUGINS_DEV_CONFIG.md)
-- **Admin guide:** See [DEVICE_MANAGEMENT.md](DEVICE_MANAGEMENT.md)
-
----
-
-**Quick Start:** Find a device field you want to protect → Click the lock icon → That's it! The field won't change until you unlock it.
+- [Device locking](./DEVICE_FIELD_LOCK.md)
+- [Device source fields](./DEVICE_SOURCE_FIELDS.md)
+- [API Device Endpoints Documentation](./API_DEVICE.md)
+- [Authoritative Field Updates System](./PLUGINS_DEV.md#authoritative-fields)
+- [Plugin Configuration Reference](./PLUGINS_DEV_CONFIG.md)
+- [Device locking APIs](API_DEVICE_FIELD_LOCK.md)
+- [Device management](DEVICE_MANAGEMENT.md)
diff --git a/docs/DEVICE_SOURCE_FIELDS.md b/docs/DEVICE_SOURCE_FIELDS.md
new file mode 100644
index 00000000..02ef5e46
--- /dev/null
+++ b/docs/DEVICE_SOURCE_FIELDS.md
@@ -0,0 +1,67 @@
+# Understanding Device Source Fields and Field Updates
+
+When the system scans a network, it finds various details about devices (like names, IP addresses, and manufacturers). To ensure the data remains accurate without accidentally overwriting manual changes, the system uses a set of "Source Rules."
+
+
+
+---
+
+## The "Protection" Levels
+
+Every piece of information for a device has a **Source**. This source determines whether a new scan is allowed to change that value.
+
+| Source Status | Description | Can a Scan Overwrite it? |
+| --- | --- | --- |
+| **USER** | You manually entered this value. | **Never** |
+| **LOCKED** | This value is pinned and protected. | **Never** |
+| **NEWDEV** | This value was initialized from `NEWDEV` plugin settings. | **Always** |
+| **(Plugin Name)** | The value was found by a specific scanner (e.g., `NBTSCAN`). | **Only if specific rules are met** |
+
+---
+
+## How Scans Update Information
+
+If a field is **not** protected by a `USER` or `LOCKED` status, the system follows these rules to decide if it should update the info:
+
+### 1. The "Empty Field" Rule (Default)
+
+By default, the system is cautious. It will only fill in a piece of information if the current field is **empty** (showing as "unknown," "0.0.0.0," or blank). It won't change for example an existing name unless you tell it to.
+
+### 2. SET_ALWAYS
+
+Some plugins are configured to be "authoritative." If a field is in the **SET_ALWAYS** setting of a plugin:
+
+* The scanner will **always** overwrite the current value with the new one.
+* *Note: It will still never overwrite a `USER` or `LOCKED` field.*
+
+### 3. SET_EMPTY
+
+If a field is in the **SET_EMPTY** list:
+
+* The scanner will **only** provide a value if the current field is currently empty.
+* This is used for fields where we want to "fill in the blanks" but never change a value once it has been established by any source.
+
+### 4. Automatic Overrides (Live Tracking)
+
+Some fields, like **IP Addresses** (`devLastIP`) and **Full Domain Names** (`devFQDN`), are set to automatically update whenever they change. This ensures that if a device moves to a new IP on your network, the system reflects that change immediately without you having to do anything.
+
+---
+
+## Summary of Field Logic
+
+| If the current value is... | And the Scan finds... | Does it update? |
+| --- | --- | --- |
+| **USER / LOCKED** | Anything | **No** |
+| **Empty** | A new value | **Yes** |
+| **A "Plugin" value** | A different value | **No** (Unless `SET_ALWAYS` is on) |
+| **An IP Address** | A different IP | **Yes** (Updates automatically) |
+
+## See also:
+
+- [Device locking](./DEVICE_FIELD_LOCK.md)
+- [Device source fields](./DEVICE_SOURCE_FIELDS.md)
+- [API Device Endpoints Documentation](./API_DEVICE.md)
+- [Authoritative Field Updates System](./PLUGINS_DEV.md#authoritative-fields)
+- [Plugin Configuration Reference](./PLUGINS_DEV_CONFIG.md)
+- [Device locking APIs](API_DEVICE_FIELD_LOCK.md)
+- [Device management](DEVICE_MANAGEMENT.md)
diff --git a/docs/img/netalertx_docs.png b/docs/img/netalertx_docs.png
index 27a0b661..39914c59 100755
Binary files a/docs/img/netalertx_docs.png and b/docs/img/netalertx_docs.png differ
diff --git a/front/deviceDetailsEdit.php b/front/deviceDetailsEdit.php
index 19e51351..d2689866 100755
--- a/front/deviceDetailsEdit.php
+++ b/front/deviceDetailsEdit.php
@@ -339,7 +339,7 @@ function getDeviceData() {
- ${generateFormHtml(settingsData, setting, fieldData.toString(), fieldOptionsOverride, null)}
+ ${generateFormHtml(settingsData, setting, fieldData.toString(), fieldOptionsOverride, null, mac == "new")}
${inlineControl}
`;
diff --git a/front/devices.php b/front/devices.php
index f6c374b1..61d91020 100755
--- a/front/devices.php
+++ b/front/devices.php
@@ -543,7 +543,10 @@ function mapColumnIndexToFieldName(index, tableColumnVisible) {
"devCustomProps", // 26
"devFQDN", // 27
"devParentRelType", // 28
- "devReqNicsOnline" // 29
+ "devReqNicsOnline", // 29
+ "devVlan", // 30
+ "devPrimaryIPv4", // 31
+ "devPrimaryIPv6", // 32
];
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
@@ -660,6 +663,9 @@ function initializeDatatable (status) {
devFQDN
devParentRelType
devReqNicsOnline
+ devVlan
+ devPrimaryIPv4
+ devPrimaryIPv6
}
count
}
@@ -743,7 +749,10 @@ function initializeDatatable (status) {
device.devCustomProps || "",
device.devFQDN || "",
device.devParentRelType || "",
- device.devReqNicsOnline || 0
+ device.devReqNicsOnline || 0,
+ device.devVlan || "",
+ device.devPrimaryIPv4 || "",
+ device.devPrimaryIPv6 || "",
];
const newRow = [];
diff --git a/front/img/svg/netalertx_blue_docs.svg b/front/img/svg/netalertx_blue_docs.svg
new file mode 100644
index 00000000..77710b9b
--- /dev/null
+++ b/front/img/svg/netalertx_blue_docs.svg
@@ -0,0 +1,453 @@
+
+
+
+
diff --git a/front/js/common.js b/front/js/common.js
index 8ff405fd..da0f2cb8 100755
--- a/front/js/common.js
+++ b/front/js/common.js
@@ -181,7 +181,7 @@ function getSettingOptions (key) {
if (result == "")
{
- console.log(`Setting options with key "${key}" not found`)
+ // console.log(`Setting options with key "${key}" not found`)
result = []
}
@@ -197,10 +197,10 @@ function getSetting (key) {
result = getCache(`nax_set_${key}`, true);
- if (result == "")
- {
- console.log(`Setting with key "${key}" not found`)
- }
+ // if (result == "")
+ // {
+ // console.log(`Setting with key "${key}" not found`)
+ // }
return result;
}
diff --git a/front/js/modal.js b/front/js/modal.js
index d4024d02..0c4ec1fb 100755
--- a/front/js/modal.js
+++ b/front/js/modal.js
@@ -170,7 +170,8 @@ function showModalPopupForm(
curValue = null,
popupFormJson = null,
parentSettingKey = null,
- triggeredBy = null
+ triggeredBy = null,
+ populateFromOverrides = true
) {
// set captions
prefix = "modal-form";
@@ -229,7 +230,8 @@ function showModalPopupForm(
setObj,
null,
fieldOptionsOverride,
- null
+ null,
+ populateFromOverrides // is new entry
)}
diff --git a/front/js/settings_utils.js b/front/js/settings_utils.js
index 2f713b73..dfafde44 100755
--- a/front/js/settings_utils.js
+++ b/front/js/settings_utils.js
@@ -321,7 +321,8 @@ function addViaPopupForm(element) {
null, // curValue
popupFormJson, // popupform
toId, // parentSettingKey
- element // triggeredBy
+ element, // triggeredBy
+ true // initialize defaut values
);
// flag something changes to prevent navigating from page
@@ -475,7 +476,8 @@ function initListInteractionOptions(element) {
curValue, // curValue
popupFormJson, // popupform
toId, // parentSettingKey
- this // triggeredBy
+ this, // triggeredBy
+ true // populate overrides
);
} else {
// Fallback to normal field input
@@ -1132,24 +1134,44 @@ function collectSetting(prefix, setCodeName, setType, settingsArray) {
// ------------------------------------------------------------------------------
// Generate the form control for setting
-function generateFormHtml(settingsData, set, overrideValue, overrideOptions, originalSetKey) {
+/**
+ * Generates the HTML string for form controls based on setting configurations.
+ * Supports various element types including select, input, button, textarea, span, and recursive datatables.
+ * * @param {Object} settingsData - The global settings object containing configuration for all available settings.
+ * @param {Object} set - The specific configuration object for the current setting.
+ * @param {string} set.setKey - Unique identifier for the setting.
+ * @param {string} set.setType - JSON string defining the UI components (dataType, elements, etc.).
+ * @param {string} [set.setValue] - The default value for the setting.
+ * @param {Array|string} [set.setEvents] - List of event triggers to be rendered as clickable icons.
+ * @param {any} overrideValue - The current value to be displayed in the form control.
+ * @param {any} overrideOptions - Custom options to override the default setting options (used primarily for dropdowns).
+ * @param {string} originalSetKey - The base key name (used to maintain reference when keys are modified for uniqueness in tables).
+ * @param {boolean} populateFromOverrides - Flag to determine if the value should be pulled from `set['setValue']` (true) or `overrideValue` (false).
+ * * @returns {string} A string of HTML containing the form elements and any associated event action icons.
+ * * @example
+ * const html = generateFormHtml(allSettings, currentSet, "DefaultVal", null, "my_key", false);
+ * $('#container').html(html);
+ */
+function generateFormHtml(settingsData, set, overrideValue, overrideOptions, originalSetKey, populateFromOverrides) {
let inputHtml = '';
// if override value is considered empty initialize from setting defaults
- overrideValue == null || overrideValue == undefined ? inVal = set['setValue'] : inVal = overrideValue
+ populateFromOverrides ? inVal = set['setValue'] : inVal = overrideValue;
+
const setKey = set['setKey'];
const setType = set['setType'];
- // if (setKey == 'NEWDEV_devParentMAC') {
+ // if (setKey == 'UNIFIAPI_site_name') {
- // console.log("==== DEBUG OUTPUT BELOW 1 ====");
- // console.log(setType);
- // console.log(setKey);
- // console.log(overrideValue);
- // console.log(inVal);
-
- // }
+ // console.log("==== DEBUG OUTPUT BELOW 1 ====");
+ // console.log("populateFromOverrides: " + populateFromOverrides);
+ // console.log(setType);
+ // console.log(setKey);
+ // console.log("overrideValue:" + overrideValue);
+ // console.log("inVal:" + inVal);
+ // console.log("set['setValue']:" + set['setValue']);
+ // }
// Parse the setType JSON string
// console.log(processQuotes(setType));
@@ -1189,15 +1211,14 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
// Override value
let val = valRes;
- // if (setKey == 'NEWDEV_devParentMAC') {
+ // if (setKey == 'UNIFIAPI_site_name') {
// console.log("==== DEBUG OUTPUT BELOW 2 ====");
// console.log(setType);
// console.log(setKey);
- // console.log(overrideValue);
- // console.log(inVal);
- // console.log(val);
-
+ // console.log("overrideValue:" + overrideValue);
+ // console.log("inVal:" + inVal);
+ // console.log("val:" + val);
// }
// Generate HTML based on elementType
@@ -1227,7 +1248,7 @@ function generateFormHtml(settingsData, set, overrideValue, overrideOptions, ori
break;
case 'input':
- const checked = val === 'True' || val === 'true' || val === '1' ? 'checked' : '';
+ const checked = val === 'True' || val === 'true' || val === '1' || val == true ? 'checked' : '';
const inputClass = inputType === 'checkbox' ? 'checkbox' : 'form-control';
inputHtml += ` ${cellHtml}
`;
diff --git a/front/js/sse_manager.js b/front/js/sse_manager.js
index 8425b306..3afda1c6 100644
--- a/front/js/sse_manager.js
+++ b/front/js/sse_manager.js
@@ -145,14 +145,18 @@ class NetAlertXStateManager {
.attr('data-version', version);
// 3. Update Build Timestamp placeholders
- const buildTime = appState["buildTimestamp"] !== undefined ? appState["buildTimestamp"] : "";
- const displayTime = (buildTime === 0) ? "UNKNOWN" : buildTime;
+ const buildTime = appState["buildTimestamp"] || 0;
+ const displayTime = buildTime ? localizeTimestamp(buildTime) : "UNKNOWN";
+
+ $('[data-plc="build-timestamp"]')
+ .html(displayTime)
+ .attr('data-build-time', buildTime);
$('[data-plc="build-timestamp"]')
.html(displayTime)
.attr('data-build-time', buildTime);
- console.log("[NetAlertX State] UI updated via jQuery");
+ // console.log("[NetAlertX State] UI updated via jQuery");
} catch (e) {
console.error("[NetAlertX State] Failed to update state display:", e);
}
diff --git a/front/js/ui_components.js b/front/js/ui_components.js
index 649531c6..80710fe1 100755
--- a/front/js/ui_components.js
+++ b/front/js/ui_components.js
@@ -668,7 +668,10 @@ function getColumnNameFromLangString(headStringKey) {
"Device_TableHead_CustomProps": "devCustomProps",
"Device_TableHead_FQDN": "devFQDN",
"Device_TableHead_ParentRelType": "devParentRelType",
- "Device_TableHead_ReqNicsOnline": "devReqNicsOnline"
+ "Device_TableHead_ReqNicsOnline": "devReqNicsOnline",
+ "Device_TableHead_Vlan": "devVlan",
+ "Device_TableHead_IPv4": "devPrimaryIPv4",
+ "Device_TableHead_IPv6": "devPrimaryIPv6"
};
return columnNameMap[headStringKey] || "";
diff --git a/front/php/components/devices_filters.php b/front/php/components/devices_filters.php
index c66492ec..0a376b4f 100755
--- a/front/php/components/devices_filters.php
+++ b/front/php/components/devices_filters.php
@@ -18,7 +18,7 @@ function renderFilterDropdown($headerKey, $columnName, $values) {
// Generate the dropdown HTML
return '
-
+
diff --git a/front/php/templates/language/ar_ar.json b/front/php/templates/language/ar_ar.json
index e97400ba..9fb0111e 100644
--- a/front/php/templates/language/ar_ar.json
+++ b/front/php/templates/language/ar_ar.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "أول جلسة",
"Device_TableHead_GUID": "معرف فريد",
"Device_TableHead_Group": "المجموعة",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "الأيقونة",
"Device_TableHead_LastIP": "آخر عنوان IP",
"Device_TableHead_LastIPOrder": "ترتيب آخر عنوان IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "اسم عقدة المزامنة",
"Device_TableHead_Type": "النوع",
"Device_TableHead_Vendor": "المصنع",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "ليس جهاز شبكة",
"Device_Table_info": "معلومات الجدول",
"Device_Table_nav_next": "التالي",
@@ -783,4 +786,4 @@
"settings_system_label": "نظام",
"settings_update_item_warning": "قم بتحديث القيمة أدناه. احرص على اتباع التنسيق السابق. لم يتم إجراء التحقق.",
"test_event_tooltip": "احفظ التغييرات أولاً قبل اختبار الإعدادات."
-}
\ No newline at end of file
+}
diff --git a/front/php/templates/language/ca_ca.json b/front/php/templates/language/ca_ca.json
index 1103b72c..a02d4e10 100644
--- a/front/php/templates/language/ca_ca.json
+++ b/front/php/templates/language/ca_ca.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Primera Sessió",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grup",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Icona",
"Device_TableHead_LastIP": "Darrera IP",
"Device_TableHead_LastIPOrder": "Últim Ordre d'IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Node Sync",
"Device_TableHead_Type": "Tipus",
"Device_TableHead_Vendor": "Venedor",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "No configurat com a dispositiu de xarxa",
"Device_Table_info": "Mostrant _INICI_ a_FINAL_ d'entrades_ TOTALS",
"Device_Table_nav_next": "Següent",
diff --git a/front/php/templates/language/cs_cz.json b/front/php/templates/language/cs_cz.json
index 5bad90f8..18609abe 100644
--- a/front/php/templates/language/cs_cz.json
+++ b/front/php/templates/language/cs_cz.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "",
"Device_TableHead_GUID": "",
"Device_TableHead_Group": "",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "",
"Device_TableHead_LastIP": "",
"Device_TableHead_LastIPOrder": "",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "",
"Device_TableHead_Type": "",
"Device_TableHead_Vendor": "",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "",
"Device_Table_info": "",
"Device_Table_nav_next": "",
diff --git a/front/php/templates/language/de_de.json b/front/php/templates/language/de_de.json
index 8d59d79d..fdbf364a 100644
--- a/front/php/templates/language/de_de.json
+++ b/front/php/templates/language/de_de.json
@@ -230,6 +230,8 @@
"Device_TableHead_FirstSession": "Erste Sitzung",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Gruppe",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Icon",
"Device_TableHead_LastIP": "Letzte IP",
"Device_TableHead_LastIPOrder": "Letzte erhaltene IP",
@@ -253,6 +255,7 @@
"Device_TableHead_SyncHubNodeName": "Synchronisationsknoten",
"Device_TableHead_Type": "Typ",
"Device_TableHead_Vendor": "Hersteller",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Nicht konfiguriert als Netzwerkgerät",
"Device_Table_info": "Zeige _START_ bis _END_ von _TOTAL_ Einträgen",
"Device_Table_nav_next": "Nächste",
diff --git a/front/php/templates/language/en_us.json b/front/php/templates/language/en_us.json
index 36ce9c7e..ba88789f 100755
--- a/front/php/templates/language/en_us.json
+++ b/front/php/templates/language/en_us.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "First Session",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Group",
+ "Device_TableHead_IPv4": "IPv4",
+ "Device_TableHead_IPv6": "IPv6",
"Device_TableHead_Icon": "Icon",
"Device_TableHead_LastIP": "Last IP",
"Device_TableHead_LastIPOrder": "Last IP Order",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Sync Node",
"Device_TableHead_Type": "Type",
"Device_TableHead_Vendor": "Vendor",
+ "Device_TableHead_Vlan": "VLAN",
"Device_Table_Not_Network_Device": "Not configured as a network device",
"Device_Table_info": "Showing _START_ to _END_ of _TOTAL_ entries",
"Device_Table_nav_next": "Next",
diff --git a/front/php/templates/language/es_es.json b/front/php/templates/language/es_es.json
index e71e127b..e7e9bf03 100644
--- a/front/php/templates/language/es_es.json
+++ b/front/php/templates/language/es_es.json
@@ -228,6 +228,8 @@
"Device_TableHead_FirstSession": "1ra. sesión",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grupo",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Icon",
"Device_TableHead_LastIP": "Última IP",
"Device_TableHead_LastIPOrder": "Última orden de IP",
@@ -251,6 +253,7 @@
"Device_TableHead_SyncHubNodeName": "Nodo de sincronización",
"Device_TableHead_Type": "Tipo",
"Device_TableHead_Vendor": "Fabricante",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "No está configurado como dispositivo de red",
"Device_Table_info": "Mostrando el INICIO y el FINAL de TODAS las entradas",
"Device_Table_nav_next": "Siguiente",
diff --git a/front/php/templates/language/fa_fa.json b/front/php/templates/language/fa_fa.json
index 8fe5e7be..3ed24919 100644
--- a/front/php/templates/language/fa_fa.json
+++ b/front/php/templates/language/fa_fa.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "",
"Device_TableHead_GUID": "",
"Device_TableHead_Group": "",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "",
"Device_TableHead_LastIP": "",
"Device_TableHead_LastIPOrder": "",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "",
"Device_TableHead_Type": "",
"Device_TableHead_Vendor": "",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "",
"Device_Table_info": "",
"Device_Table_nav_next": "",
diff --git a/front/php/templates/language/fr_fr.json b/front/php/templates/language/fr_fr.json
index ed5045a7..e32181e0 100644
--- a/front/php/templates/language/fr_fr.json
+++ b/front/php/templates/language/fr_fr.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Première session",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Groupe",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Icône",
"Device_TableHead_LastIP": "Dernière IP",
"Device_TableHead_LastIPOrder": "Ordre dernière IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Noeud de synchro",
"Device_TableHead_Type": "Type",
"Device_TableHead_Vendor": "Fabriquant",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Non configuré comme appareil du réseau",
"Device_Table_info": "Affiche de _START_ à _END_ sur _TOTAL_ entrées",
"Device_Table_nav_next": "Suivant",
@@ -783,4 +786,4 @@
"settings_system_label": "Système",
"settings_update_item_warning": "Mettre à jour la valeur ci-dessous. Veillez à bien suivre le même format qu'auparavant. Il n'y a pas de pas de contrôle.",
"test_event_tooltip": "Enregistrer d'abord vos modifications avant de tester vôtre paramétrage."
-}
+}
\ No newline at end of file
diff --git a/front/php/templates/language/it_it.json b/front/php/templates/language/it_it.json
index 9388f4c4..8ababebf 100644
--- a/front/php/templates/language/it_it.json
+++ b/front/php/templates/language/it_it.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Prima sessione",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Gruppo",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Icona",
"Device_TableHead_LastIP": "Ultimo IP",
"Device_TableHead_LastIPOrder": "Ordina per ultimo IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Sincronizza nodo",
"Device_TableHead_Type": "Tipo",
"Device_TableHead_Vendor": "Produttore",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Non configurato come dispositivo di rete",
"Device_Table_info": "Visualizzazione da _START_ a _END_ di _TOTAL_ voci",
"Device_Table_nav_next": "Successivo",
@@ -783,4 +786,4 @@
"settings_system_label": "Sistema",
"settings_update_item_warning": "Aggiorna il valore qui sotto. Fai attenzione a seguire il formato precedente. La convalida non viene eseguita.",
"test_event_tooltip": "Salva le modifiche prima di provare le nuove impostazioni."
-}
+}
\ No newline at end of file
diff --git a/front/php/templates/language/ja_jp.json b/front/php/templates/language/ja_jp.json
index e796cc39..e5dece69 100644
--- a/front/php/templates/language/ja_jp.json
+++ b/front/php/templates/language/ja_jp.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "初回セッション",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "グループ",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "アイコン",
"Device_TableHead_LastIP": "直近のIP",
"Device_TableHead_LastIPOrder": "直近のIP順",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "同期ノード",
"Device_TableHead_Type": "種別",
"Device_TableHead_Vendor": "ベンダー",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "ネットワーク機器として構成されていない",
"Device_Table_info": "_START_~_END_を表示 / _TOTAL_ 件中",
"Device_Table_nav_next": "次",
diff --git a/front/php/templates/language/nb_no.json b/front/php/templates/language/nb_no.json
index aba15936..7b1d40aa 100644
--- a/front/php/templates/language/nb_no.json
+++ b/front/php/templates/language/nb_no.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Første Økt",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Gruppe",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Ikon",
"Device_TableHead_LastIP": "Siste IP",
"Device_TableHead_LastIPOrder": "Siste IP Rekkefølge",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Synkroniser Node",
"Device_TableHead_Type": "Type",
"Device_TableHead_Vendor": "Leverandør",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Ikke konfigurert som en nettverksenhet",
"Device_Table_info": "Showing _START_ to _END_ of _TOTAL_ entries",
"Device_Table_nav_next": "Neste",
diff --git a/front/php/templates/language/pl_pl.json b/front/php/templates/language/pl_pl.json
index f6896615..14d8fbe5 100644
--- a/front/php/templates/language/pl_pl.json
+++ b/front/php/templates/language/pl_pl.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Pierwsza sesja",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grupa",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Ikona",
"Device_TableHead_LastIP": "Ostatni adres IP",
"Device_TableHead_LastIPOrder": "Kolejność ostatniego adresu IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Węzeł synchronizacji",
"Device_TableHead_Type": "Typ",
"Device_TableHead_Vendor": "Producent",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Nie skonfigurowano jako urządzenie sieciowe",
"Device_Table_info": "Pokazuje _START_ do _END_ z _TOTAL_ wpisów",
"Device_Table_nav_next": "Następna",
diff --git a/front/php/templates/language/pt_br.json b/front/php/templates/language/pt_br.json
index 8e3dae21..02244aea 100644
--- a/front/php/templates/language/pt_br.json
+++ b/front/php/templates/language/pt_br.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Primeira sessão",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grupo",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Ícone",
"Device_TableHead_LastIP": "Último IP",
"Device_TableHead_LastIPOrder": "Último pedido de IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Nó de sincronização",
"Device_TableHead_Type": "Tipo",
"Device_TableHead_Vendor": "Fornecedor",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Não configurado como um dispositivo de rede",
"Device_Table_info": "Mostrando _START_ de _END_ do _TOTAL_ entradas",
"Device_Table_nav_next": "Próximo",
diff --git a/front/php/templates/language/pt_pt.json b/front/php/templates/language/pt_pt.json
index c3ad2246..687a9009 100644
--- a/front/php/templates/language/pt_pt.json
+++ b/front/php/templates/language/pt_pt.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Primeira sessão",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grupo",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Ícone",
"Device_TableHead_LastIP": "Último IP",
"Device_TableHead_LastIPOrder": "Último pedido de IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Nó de sincronização",
"Device_TableHead_Type": "Tipo",
"Device_TableHead_Vendor": "Fornecedor",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Não configurado como um dispositivo de rede",
"Device_Table_info": "A mostrar _START_ to _END_ of _TOTAL_ entradas",
"Device_Table_nav_next": "Próximo",
diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json
index 6d8c48e4..dd868dbe 100644
--- a/front/php/templates/language/ru_ru.json
+++ b/front/php/templates/language/ru_ru.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Первый сеанс",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Группа",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Значок",
"Device_TableHead_LastIP": "Последний IP",
"Device_TableHead_LastIPOrder": "Последний IP-запрос",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Узел синхронизации",
"Device_TableHead_Type": "Тип",
"Device_TableHead_Vendor": "Поставщик",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Не настроено как сетевое устройство",
"Device_Table_info": "Показаны с _START_ по _END_ из _TOTAL_ записей",
"Device_Table_nav_next": "Следующая",
diff --git a/front/php/templates/language/sv_sv.json b/front/php/templates/language/sv_sv.json
index f76e6729..ae8afffd 100644
--- a/front/php/templates/language/sv_sv.json
+++ b/front/php/templates/language/sv_sv.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "",
"Device_TableHead_GUID": "",
"Device_TableHead_Group": "",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "",
"Device_TableHead_LastIP": "",
"Device_TableHead_LastIPOrder": "",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "",
"Device_TableHead_Type": "",
"Device_TableHead_Vendor": "",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "",
"Device_Table_info": "",
"Device_Table_nav_next": "",
diff --git a/front/php/templates/language/tr_tr.json b/front/php/templates/language/tr_tr.json
index e96496ba..ad873c21 100644
--- a/front/php/templates/language/tr_tr.json
+++ b/front/php/templates/language/tr_tr.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "İlk Oturum",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grup",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "İkon",
"Device_TableHead_LastIP": "Son IP",
"Device_TableHead_LastIPOrder": "Son IP Sırası",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Senkronizasyon Node",
"Device_TableHead_Type": "Tür",
"Device_TableHead_Vendor": "Üretici",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Ağ cihazı olarak ayarlanmadı",
"Device_Table_info": "Showing _START_ to _END_ of _TOTAL_ entries",
"Device_Table_nav_next": "Sonraki",
diff --git a/front/php/templates/language/uk_ua.json b/front/php/templates/language/uk_ua.json
index 0a3ed72e..78006829 100644
--- a/front/php/templates/language/uk_ua.json
+++ b/front/php/templates/language/uk_ua.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "Перша сесія",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Група",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "Значок",
"Device_TableHead_LastIP": "Останній IP",
"Device_TableHead_LastIPOrder": "Останнє замовлення IP",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "Вузол синхронізації",
"Device_TableHead_Type": "Тип",
"Device_TableHead_Vendor": "Продавець",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "Не налаштовано як мережевий пристрій",
"Device_Table_info": "Показано від _START_ до _END_ із _TOTAL_ записів",
"Device_Table_nav_next": "Далі",
@@ -783,4 +786,4 @@
"settings_system_label": "Система",
"settings_update_item_warning": "Оновіть значення нижче. Слідкуйте за попереднім форматом. Перевірка не виконана.",
"test_event_tooltip": "Перш ніж перевіряти налаштування, збережіть зміни."
-}
+}
\ No newline at end of file
diff --git a/front/php/templates/language/zh_cn.json b/front/php/templates/language/zh_cn.json
index dae234b5..1139be4b 100644
--- a/front/php/templates/language/zh_cn.json
+++ b/front/php/templates/language/zh_cn.json
@@ -226,6 +226,8 @@
"Device_TableHead_FirstSession": "加入",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "组",
+ "Device_TableHead_IPv4": "",
+ "Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "图标",
"Device_TableHead_LastIP": "上次 IP",
"Device_TableHead_LastIPOrder": "上次 IP 排序",
@@ -249,6 +251,7 @@
"Device_TableHead_SyncHubNodeName": "同步节点",
"Device_TableHead_Type": "类型",
"Device_TableHead_Vendor": "制造商",
+ "Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "未配置为网络设备",
"Device_Table_info": "显示第_START_至 END_条_共_TOTAL_条",
"Device_Table_nav_next": "下一页",
diff --git a/front/plugins/newdev_template/config.json b/front/plugins/newdev_template/config.json
index 0f01158d..22de174f 100755
--- a/front/plugins/newdev_template/config.json
+++ b/front/plugins/newdev_template/config.json
@@ -1588,6 +1588,38 @@
}
]
},
+ {
+ "function": "devVlan",
+ "type": {
+ "dataType": "string",
+ "elements": [
+ {
+ "elementType": "input",
+ "elementOptions": [],
+ "transformers": []
+ }
+ ]
+ },
+ "maxLength": 50,
+ "default_value": "",
+ "options": [],
+ "localized": [
+ "name",
+ "description"
+ ],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "VLAN"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "The VLAN identifier or name the device belongs to. Database column name: devVlan."
+ }
+ ]
+ },
{
"function": "devSyncHubNode",
"type": {
@@ -1901,38 +1933,6 @@
}
]
},
- {
- "function": "devVlan",
- "type": {
- "dataType": "string",
- "elements": [
- {
- "elementType": "input",
- "elementOptions": [],
- "transformers": []
- }
- ]
- },
- "maxLength": 50,
- "default_value": "",
- "options": [],
- "localized": [
- "name",
- "description"
- ],
- "name": [
- {
- "language_code": "en_us",
- "string": "VLAN"
- }
- ],
- "description": [
- {
- "language_code": "en_us",
- "string": "The VLAN identifier or name the device belongs to. Database column name: devVlan."
- }
- ]
- },
{
"function": "devForceStatus",
"type": {
diff --git a/front/plugins/ui_settings/config.json b/front/plugins/ui_settings/config.json
index f4f5c1de..18cee2cb 100755
--- a/front/plugins/ui_settings/config.json
+++ b/front/plugins/ui_settings/config.json
@@ -440,7 +440,10 @@
"Device_TableHead_CustomProps",
"Device_TableHead_FQDN",
"Device_TableHead_ParentRelType",
- "Device_TableHead_ReqNicsOnline"
+ "Device_TableHead_ReqNicsOnline",
+ "Device_TableHead_Vlan",
+ "Device_TableHead_IPv4",
+ "Device_TableHead_IPv6"
],
"localized": ["name", "description"],
"name": [
@@ -517,7 +520,8 @@
"Device_TableHead_NetworkSite",
"Device_TableHead_SSID",
"Device_TableHead_SourcePlugin",
- "Device_TableHead_ParentRelType"
+ "Device_TableHead_ParentRelType",
+ "Device_TableHead_Vlan"
],
"localized": ["name", "description"],
"name": [
diff --git a/front/settings.php b/front/settings.php
index 899cdef0..2dc298bd 100755
--- a/front/settings.php
+++ b/front/settings.php
@@ -511,10 +511,10 @@ $settingsJSON_DB = json_encode($settings, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX
}
// INPUT
- inputFormHtml = generateFormHtml(settingsData, set, valIn, null, null);
+ inputFormHtml = generateFormHtml(settingsData, set, valIn, null, null, false);
- // construct final HTML for the setting
- setHtml += inputFormHtml + overrideHtml + `
+ // construct final HTML for the setting
+ setHtml += inputFormHtml + overrideHtml + `
`
diff --git a/install/production-filesystem/entrypoint.d/20-first-run-config.sh b/install/production-filesystem/entrypoint.d/20-first-run-config.sh
index 8e37f2d6..b57a657f 100755
--- a/install/production-filesystem/entrypoint.d/20-first-run-config.sh
+++ b/install/production-filesystem/entrypoint.d/20-first-run-config.sh
@@ -6,17 +6,46 @@ if [ -d "${NETALERTX_CONFIG}" ]; then
chmod u+rwX "${NETALERTX_CONFIG}" 2>/dev/null || true
fi
chmod u+rw "${NETALERTX_CONFIG}/app.conf" 2>/dev/null || true
+
+set -eu
+
+CYAN=$(printf '\033[1;36m')
+RED=$(printf '\033[1;31m')
+RESET=$(printf '\033[0m')
+
+# Ensure config folder exists
+if [ ! -d "${NETALERTX_CONFIG}" ]; then
+ if ! mkdir -p "${NETALERTX_CONFIG}"; then
+ >&2 printf "%s" "${RED}"
+ >&2 cat <&2 printf "%s" "${RESET}"
+ exit 1
+ fi
+ chmod 700 "${NETALERTX_CONFIG}" 2>/dev/null || true
+fi
+
+# Fresh rebuild requested
+if [ "${ALWAYS_FRESH_INSTALL:-false}" = "true" ] && [ -e "${NETALERTX_CONFIG}/app.conf" ]; then
+ >&2 echo "INFO: ALWAYS_FRESH_INSTALL enabled — removing existing config."
+ rm -rf "${NETALERTX_CONFIG}"/*
+fi
+
# Check for app.conf and deploy if required
if [ ! -f "${NETALERTX_CONFIG}/app.conf" ]; then
- mkdir -p "${NETALERTX_CONFIG}" || {
- >&2 echo "ERROR: Failed to create config directory ${NETALERTX_CONFIG}"
- exit 1
- }
install -m 600 /app/back/app.conf "${NETALERTX_CONFIG}/app.conf" || {
>&2 echo "ERROR: Failed to deploy default config to ${NETALERTX_CONFIG}/app.conf"
exit 2
}
- RESET=$(printf '\033[0m')
+ >&2 printf "%s" "${CYAN}"
>&2 cat <&2 printf "%s" "${RESET}"
+ >&2 printf "%s" "${RESET}"
fi
diff --git a/server/api_server/graphql_endpoint.py b/server/api_server/graphql_endpoint.py
index 94c5c624..0f99dc33 100755
--- a/server/api_server/graphql_endpoint.py
+++ b/server/api_server/graphql_endpoint.py
@@ -339,6 +339,9 @@ class Query(ObjectType):
"devFQDN",
"devParentRelType",
"devParentMAC",
+ "devVlan",
+ "devPrimaryIPv4",
+ "devPrimaryIPv6"
]
search_term = options.search.lower()
diff --git a/server/const.py b/server/const.py
index 63594d62..8725d51e 100755
--- a/server/const.py
+++ b/server/const.py
@@ -176,6 +176,12 @@ sql_devices_filters = """
SELECT DISTINCT 'devSyncHubNode' AS columnName, devSyncHubNode AS columnValue
FROM Devices WHERE devSyncHubNode NOT IN ('', 'null') AND devSyncHubNode IS NOT NULL
UNION
+ SELECT DISTINCT 'devVlan' AS columnName, devVlan AS columnValue
+ FROM Devices WHERE devVlan NOT IN ('', 'null') AND devVlan IS NOT NULL
+ UNION
+ SELECT DISTINCT 'devParentRelType' AS columnName, devParentRelType AS columnValue
+ FROM Devices WHERE devParentRelType NOT IN ('', 'null') AND devParentRelType IS NOT NULL
+ UNION
SELECT DISTINCT 'devSSID' AS columnName, devSSID AS columnValue
FROM Devices WHERE devSSID NOT IN ('', 'null') AND devSSID IS NOT NULL
ORDER BY columnName;
diff --git a/server/db/authoritative_handler.py b/server/db/authoritative_handler.py
index 1c8b9e27..444170ac 100644
--- a/server/db/authoritative_handler.py
+++ b/server/db/authoritative_handler.py
@@ -448,4 +448,3 @@ def unlock_fields(conn, mac=None, fields=None, clear_all=False):
}
finally:
conn.close()
-
diff --git a/server/helper.py b/server/helper.py
index 4565dcd5..bfbf65c1 100755
--- a/server/helper.py
+++ b/server/helper.py
@@ -590,26 +590,6 @@ def normalize_string(text):
# MAC and IP helper methods
# -------------------------------------------------------------------------------
-
-# # -------------------------------------------------------------------------------------------
-# def is_random_mac(mac: str) -> bool:
-# """Determine if a MAC address is random, respecting user-defined prefixes not to mark as random."""
-
-# is_random = mac[1].upper() in ["2", "6", "A", "E"]
-
-# # Get prefixes from settings
-# prefixes = get_setting_value("UI_NOT_RANDOM_MAC")
-
-# # If detected as random, make sure it doesn't start with a prefix the user wants to exclude
-# if is_random:
-# for prefix in prefixes:
-# if mac.upper().startswith(prefix.upper()):
-# is_random = False
-# break
-
-# return is_random
-
-
# -------------------------------------------------------------------------------------------
def generate_mac_links(html, deviceUrl):
p = re.compile(r"(?:[0-9a-fA-F]:?){12}")
diff --git a/server/messaging/in_app.py b/server/messaging/in_app.py
index 97340c20..ca90962a 100755
--- a/server/messaging/in_app.py
+++ b/server/messaging/in_app.py
@@ -3,6 +3,7 @@ import sys
import json
import uuid
import time
+import fcntl
from flask import jsonify
@@ -19,6 +20,35 @@ from api_server.sse_broadcast import broadcast_unread_notifications_count # noq
NOTIFICATION_API_FILE = apiPath + 'user_notifications.json'
+def locked_notifications_file(callback):
+ # Ensure file exists
+ if not os.path.exists(NOTIFICATION_API_FILE):
+ with open(NOTIFICATION_API_FILE, "w") as f:
+ f.write("[]")
+
+ with open(NOTIFICATION_API_FILE, "r+") as f:
+ fcntl.flock(f, fcntl.LOCK_EX)
+ try:
+ raw = f.read().strip() or "[]"
+ try:
+ data = json.loads(raw)
+ except json.JSONDecodeError:
+ mylog("none", "[Notification] Corrupted JSON detected, resetting.")
+ data = []
+
+ # Let caller modify data
+ result = callback(data)
+
+ # Write back atomically
+ f.seek(0)
+ f.truncate()
+ json.dump(data, f, indent=4)
+
+ return result
+ finally:
+ fcntl.flock(f, fcntl.LOCK_UN)
+
+
# Show Frontend User Notification
def write_notification(content, level="alert", timestamp=None):
"""
@@ -36,50 +66,21 @@ def write_notification(content, level="alert", timestamp=None):
if timestamp is None:
timestamp = timeNowDB()
- # Generate GUID
- guid = str(uuid.uuid4())
-
- # Prepare notification dictionary
notification = {
"timestamp": str(timestamp),
- "guid": guid,
+ "guid": str(uuid.uuid4()),
"read": 0,
"level": level,
"content": content,
}
- # If file exists, load existing data, otherwise initialize as empty list
- try:
- if os.path.exists(NOTIFICATION_API_FILE):
- with open(NOTIFICATION_API_FILE, "r") as file:
- file_contents = file.read().strip()
- if file_contents:
- notifications = json.loads(file_contents)
- if not isinstance(notifications, list):
- mylog("error", "[Notification] Invalid format: not a list, resetting")
- notifications = []
- else:
- notifications = []
- else:
- notifications = []
- except Exception as e:
- mylog("error", [f"[Notification] Error reading notifications file: {e}"])
- notifications = []
+ def update(notifications):
+ notifications.append(notification)
- # Append new notification
- notifications.append(notification)
+ locked_notifications_file(update)
- # Write updated data back to file
try:
- with open(NOTIFICATION_API_FILE, "w") as file:
- json.dump(notifications, file, indent=4)
- except Exception as e:
- mylog("error", [f"[Notification] Error writing to notifications file: {e}"])
- # Don't re-raise, just log. This prevents the API from crashing 500.
-
- # Broadcast unread count update
- try:
- unread_count = sum(1 for n in notifications if n.get("read", 0) == 0)
+ unread_count = sum(1 for n in locked_notifications_file(lambda n: n) if n.get("read", 0) == 0)
broadcast_unread_notifications_count(unread_count)
except Exception as e:
mylog("none", [f"[Notification] Failed to broadcast unread count: {e}"])
@@ -147,24 +148,42 @@ def mark_all_notifications_read():
"error": str (optional)
}
"""
+ # If notifications file does not exist, nothing to mark
if not os.path.exists(NOTIFICATION_API_FILE):
return {"success": True}
try:
- with open(NOTIFICATION_API_FILE, "r") as f:
- notifications = json.load(f)
- except Exception as e:
- mylog("none", f"[Notification] Failed to read notifications: {e}")
- return {"success": False, "error": str(e)}
+ # Open file in read/write mode and acquire exclusive lock
+ with open(NOTIFICATION_API_FILE, "r+") as f:
+ fcntl.flock(f, fcntl.LOCK_EX)
- for n in notifications:
- n["read"] = 1
+ try:
+ # Read file contents
+ file_contents = f.read().strip()
+ if file_contents == "":
+ notifications = []
+ else:
+ try:
+ notifications = json.loads(file_contents)
+ except json.JSONDecodeError as e:
+ mylog("none", f"[Notification] Corrupted notifications JSON: {e}")
+ notifications = []
+
+ # Mark all notifications as read
+ for n in notifications:
+ n["read"] = 1
+
+ # Rewrite file safely
+ f.seek(0)
+ f.truncate()
+ json.dump(notifications, f, indent=4)
+
+ finally:
+ # Always release file lock
+ fcntl.flock(f, fcntl.LOCK_UN)
- try:
- with open(NOTIFICATION_API_FILE, "w") as f:
- json.dump(notifications, f, indent=4)
except Exception as e:
- mylog("none", f"[Notification] Failed to write notifications: {e}")
+ mylog("none", f"[Notification] Failed to read/write notifications: {e}")
return {"success": False, "error": str(e)}
mylog("debug", "[Notification] All notifications marked as read.")
diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py
index 797ed9ff..972ac25f 100755
--- a/server/scan/device_handling.py
+++ b/server/scan/device_handling.py
@@ -110,6 +110,7 @@ FIELD_SPECS = {
"source_col": "devNameSource",
"empty_values": ["", "null", "(unknown)", "(name not found)"],
"priority": ["NSLOOKUP", "AVAHISCAN", "NBTSCAN", "DIGSCAN", "ARPSCAN", "DHCPLSS", "NEWDEV", "N/A"],
+ "allow_override_if_changed": True,
},
# ==========================================================
diff --git a/server/scan/device_heuristics.py b/server/scan/device_heuristics.py
index 15f9a0ad..b9c14520 100755
--- a/server/scan/device_heuristics.py
+++ b/server/scan/device_heuristics.py
@@ -5,6 +5,7 @@ import base64
from pathlib import Path
from typing import Optional, Tuple
from logger import mylog
+from helper import is_random_mac
# Register NetAlertX directories
INSTALL_PATH = os.getenv("NETALERTX_APP", "/app")
@@ -183,17 +184,21 @@ def guess_device_attributes(
type_ = None
icon = None
- # --- Strict MAC + vendor rule matching from external file ---
+ # 1. Try strict MAC match first
type_, icon = match_mac_and_vendor(mac_clean, vendor, default_type, default_icon)
+ # 2. If no strict match, try Name match BEFORE checking for random MAC
+ if not type_ or type_ == default_type:
+ type_, icon = match_name(name, default_type, default_icon)
+
+ # 3. Only if it's STILL not found, apply the Random MAC block
+ if type_ == default_type and is_random_mac(mac):
+ return default_icon, default_type
+
# --- Loose Vendor-based fallback ---
if not type_ or type_ == default_type:
type_, icon = match_vendor(vendor, default_type, default_icon)
- # --- Loose Name-based fallback ---
- if not type_ or type_ == default_type:
- type_, icon = match_name(name, default_type, default_icon)
-
# --- Loose IP-based fallback ---
if (not type_ or type_ == default_type) or (not icon or icon == default_icon):
type_, icon = match_ip(ip, default_type, default_icon)
@@ -261,4 +266,4 @@ def guess_type(
# Handler for when this is run as a program instead of called as a module.
if __name__ == "__main__":
- mylog("error", "This module is not intended to be run directly.")
+ mylog("none", "This module is not intended to be run directly.")