diff --git a/front/plugins/__template/config.json b/front/plugins/__template/config.json
index 80b2c64c..dde5a747 100755
--- a/front/plugins/__template/config.json
+++ b/front/plugins/__template/config.json
@@ -333,6 +333,68 @@
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/adguard_import/config.json b/front/plugins/adguard_import/config.json
index 8a9e8530..a9f60663 100644
--- a/front/plugins/adguard_import/config.json
+++ b/front/plugins/adguard_import/config.json
@@ -307,6 +307,68 @@
"string": "Some devices don't have a MAC assigned. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped."
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/arp_scan/config.json b/front/plugins/arp_scan/config.json
index 842069c9..f0f8e4d2 100755
--- a/front/plugins/arp_scan/config.json
+++ b/front/plugins/arp_scan/config.json
@@ -274,7 +274,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
@@ -305,7 +305,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/asuswrt_import/config.json b/front/plugins/asuswrt_import/config.json
index a4678ceb..c4d54a83 100755
--- a/front/plugins/asuswrt_import/config.json
+++ b/front/plugins/asuswrt_import/config.json
@@ -135,7 +135,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
@@ -166,7 +166,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
diff --git a/front/plugins/avahi_scan/config.json b/front/plugins/avahi_scan/config.json
index e65e6c9a..5f170004 100755
--- a/front/plugins/avahi_scan/config.json
+++ b/front/plugins/avahi_scan/config.json
@@ -89,7 +89,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
@@ -119,7 +119,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
diff --git a/front/plugins/dhcp_leases/config.json b/front/plugins/dhcp_leases/config.json
index ad94cd5d..8fe9bd86 100755
--- a/front/plugins/dhcp_leases/config.json
+++ b/front/plugins/dhcp_leases/config.json
@@ -689,7 +689,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
@@ -720,7 +720,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/dig_scan/config.json b/front/plugins/dig_scan/config.json
index 981d9d34..b97f3602 100755
--- a/front/plugins/dig_scan/config.json
+++ b/front/plugins/dig_scan/config.json
@@ -90,7 +90,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
@@ -121,7 +121,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/freebox/config.json b/front/plugins/freebox/config.json
index 0558b8ac..2252fcfe 100755
--- a/front/plugins/freebox/config.json
+++ b/front/plugins/freebox/config.json
@@ -303,6 +303,68 @@
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/icmp_scan/config.json b/front/plugins/icmp_scan/config.json
index 43b0c892..fd400adc 100755
--- a/front/plugins/icmp_scan/config.json
+++ b/front/plugins/icmp_scan/config.json
@@ -296,6 +296,68 @@
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/internet_ip/config.json b/front/plugins/internet_ip/config.json
index f55eae79..dc2a988d 100755
--- a/front/plugins/internet_ip/config.json
+++ b/front/plugins/internet_ip/config.json
@@ -410,6 +410,68 @@
"string": "Benachrichtige nur bei diesen Status. new bedeutet ein neues eindeutiges (einzigartige Kombination aus PrimaryId und SecondaryId) Objekt wurde erkennt. watched-changed bedeutet eine ausgewählte Watched_ValueN-Spalte hat sich geändert."
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/ipneigh/config.json b/front/plugins/ipneigh/config.json
index 12a53563..c80046c4 100755
--- a/front/plugins/ipneigh/config.json
+++ b/front/plugins/ipneigh/config.json
@@ -138,7 +138,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -169,7 +169,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/luci_import/config.json b/front/plugins/luci_import/config.json
index f70be1a6..8bb19489 100755
--- a/front/plugins/luci_import/config.json
+++ b/front/plugins/luci_import/config.json
@@ -391,7 +391,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -422,7 +422,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/mikrotik_scan/config.json b/front/plugins/mikrotik_scan/config.json
index a57c280d..c6964077 100755
--- a/front/plugins/mikrotik_scan/config.json
+++ b/front/plugins/mikrotik_scan/config.json
@@ -267,6 +267,68 @@
"string": "Password for Mikrotik Router"
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/nbtscan_scan/config.json b/front/plugins/nbtscan_scan/config.json
index f1e56117..d380bba5 100755
--- a/front/plugins/nbtscan_scan/config.json
+++ b/front/plugins/nbtscan_scan/config.json
@@ -89,7 +89,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -119,7 +119,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/nmap_dev_scan/config.json b/front/plugins/nmap_dev_scan/config.json
index db5b4aca..2f6c69f3 100755
--- a/front/plugins/nmap_dev_scan/config.json
+++ b/front/plugins/nmap_dev_scan/config.json
@@ -451,6 +451,68 @@
"string": "When scanning remote networks, NMAP can only retrieve the IP address, not the MAC address. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped."
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/nslookup_scan/config.json b/front/plugins/nslookup_scan/config.json
index 3de69f80..e57a4899 100755
--- a/front/plugins/nslookup_scan/config.json
+++ b/front/plugins/nslookup_scan/config.json
@@ -89,7 +89,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
@@ -120,7 +120,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
diff --git a/front/plugins/omada_sdn_imp/config.json b/front/plugins/omada_sdn_imp/config.json
index 89a26266..845cd4ed 100755
--- a/front/plugins/omada_sdn_imp/config.json
+++ b/front/plugins/omada_sdn_imp/config.json
@@ -467,6 +467,68 @@
}
]
}
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/omada_sdn_openapi/config.json b/front/plugins/omada_sdn_openapi/config.json
index dfd23285..5dd193d5 100755
--- a/front/plugins/omada_sdn_openapi/config.json
+++ b/front/plugins/omada_sdn_openapi/config.json
@@ -440,6 +440,68 @@
}
]
}
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
],
"database_column_definitions": [
diff --git a/front/plugins/pihole_api_scan/config.json b/front/plugins/pihole_api_scan/config.json
index 16283444..f80c2f55 100644
--- a/front/plugins/pihole_api_scan/config.json
+++ b/front/plugins/pihole_api_scan/config.json
@@ -121,7 +121,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -154,7 +154,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/pihole_scan/config.json b/front/plugins/pihole_scan/config.json
index 7f98770f..a72f7113 100755
--- a/front/plugins/pihole_scan/config.json
+++ b/front/plugins/pihole_scan/config.json
@@ -223,7 +223,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
@@ -253,7 +253,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true"}],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
diff --git a/front/plugins/plugin_helper.py b/front/plugins/plugin_helper.py
index 972af95e..6e1f99a0 100755
--- a/front/plugins/plugin_helper.py
+++ b/front/plugins/plugin_helper.py
@@ -184,7 +184,12 @@ def normalize_mac(mac):
:param mac: The MAC address to normalize.
:return: The normalized MAC address.
"""
- s = str(mac).upper().strip()
+ s = str(mac).strip()
+
+ if s.lower() == "internet":
+ return "Internet"
+
+ s = s.upper()
# Determine separator if present, prefer colon, then hyphen
if ':' in s:
diff --git a/front/plugins/snmp_discovery/config.json b/front/plugins/snmp_discovery/config.json
index fc194a3e..f3f5ea84 100755
--- a/front/plugins/snmp_discovery/config.json
+++ b/front/plugins/snmp_discovery/config.json
@@ -593,7 +593,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -624,7 +624,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/sync/config.json b/front/plugins/sync/config.json
index 913ab556..bc32e307 100755
--- a/front/plugins/sync/config.json
+++ b/front/plugins/sync/config.json
@@ -859,6 +859,68 @@
"string": "Status"
}
]
+ },
+ {
+ "function": "SET_ALWAYS",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": ["devMac", "devLastIP"],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set always columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
+ }
+ ]
+ },
+ {
+ "function": "SET_EMPTY",
+ "type": {
+ "dataType": "array",
+ "elements": [
+ {
+ "elementType": "select",
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
+ "transformers": []
+ }
+ ]
+ },
+ "default_value": [],
+ "options": [
+ "devMac",
+ "devLastIP"
+ ],
+ "localized": ["name", "description"],
+ "name": [
+ {
+ "language_code": "en_us",
+ "string": "Set empty columns"
+ }
+ ],
+ "description": [
+ {
+ "language_code": "en_us",
+ "string": "These columns are only overwritten if they are empty or set to NEWDEV."
+ }
+ ]
}
]
}
\ No newline at end of file
diff --git a/front/plugins/unifi_api_import/config.json b/front/plugins/unifi_api_import/config.json
index 1aedc822..9381d60f 100755
--- a/front/plugins/unifi_api_import/config.json
+++ b/front/plugins/unifi_api_import/config.json
@@ -505,7 +505,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -538,7 +538,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/unifi_import/config.json b/front/plugins/unifi_import/config.json
index d82019d2..897a2c33 100755
--- a/front/plugins/unifi_import/config.json
+++ b/front/plugins/unifi_import/config.json
@@ -923,7 +923,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -959,7 +959,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/front/plugins/vendor_update/config.json b/front/plugins/vendor_update/config.json
index 93eef6d6..eb156f5e 100755
--- a/front/plugins/vendor_update/config.json
+++ b/front/plugins/vendor_update/config.json
@@ -233,7 +233,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
@@ -265,7 +265,7 @@
"elements": [
{
"elementType": "select",
- "elementOptions": [{ "multiple": "true", "orderable": "true" }],
+ "elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
diff --git a/server/api_server/api_server_start.py b/server/api_server/api_server_start.py
index 3187596c..e509ca28 100755
--- a/server/api_server/api_server_start.py
+++ b/server/api_server/api_server_start.py
@@ -44,7 +44,7 @@ from models.user_events_queue_instance import UserEventsQueueInstance # noqa: E
from models.event_instance import EventInstance # noqa: E402 [flake8 lint suppression]
# Import tool logic from the MCP/tools module to reuse behavior (no blueprints)
-from plugin_helper import is_mac # noqa: E402 [flake8 lint suppression]
+from plugin_helper import is_mac, normalize_mac # noqa: E402 [flake8 lint suppression]
# is_mac is provided in mcp_endpoint and used by those handlers
# mcp_endpoint contains helper functions; routes moved into this module to keep a single place for routes
from messaging.in_app import ( # noqa: E402 [flake8 lint suppression]
@@ -469,33 +469,28 @@ def api_device_field_lock(mac, payload=None):
if not field_name:
return jsonify({"success": False, "error": "fieldName is required"}), 400
- # Validate that the field can be locked
- source_field = field_name + "Source"
- allowed_tracked_fields = {
- "devMac", "devName", "devLastIP", "devVendor", "devFQDN",
- "devSSID", "devParentMAC", "devParentPort", "devParentRelType", "devVlan"
- }
- if field_name not in allowed_tracked_fields:
- return jsonify({"success": False, "error": f"Field '{field_name}' cannot be locked"}), 400
-
device_handler = DeviceInstance()
+ normalized_mac = normalize_mac(mac)
try:
- # When locking: set source to LOCKED
- # When unlocking: check current value and let plugins take over
- new_source = "LOCKED" if should_lock else "NEWDEV"
- result = device_handler.updateDeviceColumn(mac, source_field, new_source)
-
- if result.get("success"):
- action = "locked" if should_lock else "unlocked"
- return jsonify({
- "success": True,
- "message": f"Field {field_name} {action}",
- "fieldName": field_name,
- "locked": should_lock
- })
+ if should_lock:
+ result = device_handler.lockDeviceField(normalized_mac, field_name)
+ action = "locked"
else:
- return jsonify(result), 400
+ result = device_handler.unlockDeviceField(normalized_mac, field_name)
+ action = "unlocked"
+
+ response = dict(result)
+ response["fieldName"] = field_name
+ response["locked"] = should_lock
+
+ if response.get("success"):
+ response.setdefault("message", f"Field {field_name} {action}")
+ return jsonify(response)
+
+ if "does not support" in response.get("error", ""):
+ response["error"] = f"Field '{field_name}' cannot be {action}"
+ return jsonify(response), 400
except Exception as e:
mylog("none", f"Error locking field {field_name} for {mac}: {str(e)}")
return jsonify({"success": False, "error": str(e)}), 500
diff --git a/server/db/authoritative_handler.py b/server/db/authoritative_handler.py
index 39f3e498..63aaed83 100644
--- a/server/db/authoritative_handler.py
+++ b/server/db/authoritative_handler.py
@@ -152,12 +152,20 @@ def enforce_source_on_user_update(devMac, updates_dict, conn):
cur = conn.cursor()
# Check if field has a corresponding source and should be updated
+ cur = conn.cursor()
+ try:
+ cur.execute("PRAGMA table_info(Devices)")
+ device_columns = {row["name"] for row in cur.fetchall()}
+ except Exception:
+ device_columns = set()
+
updates_to_apply = {}
for field_name, new_value in updates_dict.items():
if field_name in FIELD_SOURCE_MAP:
source_field = FIELD_SOURCE_MAP[field_name]
# User is updating this field, so mark it as USER
- updates_to_apply[source_field] = "USER"
+ if not device_columns or source_field in device_columns:
+ updates_to_apply[source_field] = "USER"
if not updates_to_apply:
return
@@ -198,6 +206,15 @@ def lock_field(devMac, field_name, conn):
source_field = FIELD_SOURCE_MAP[field_name]
cur = conn.cursor()
+ try:
+ cur.execute("PRAGMA table_info(Devices)")
+ device_columns = {row["name"] for row in cur.fetchall()}
+ except Exception:
+ device_columns = set()
+
+ if device_columns and source_field not in device_columns:
+ mylog("debug", [f"[lock_field] Source column {source_field} missing for {field_name}"])
+ return
sql = f"UPDATE Devices SET {source_field}='LOCKED' WHERE devMac = ?"
@@ -227,6 +244,15 @@ def unlock_field(devMac, field_name, conn):
source_field = FIELD_SOURCE_MAP[field_name]
cur = conn.cursor()
+ try:
+ cur.execute("PRAGMA table_info(Devices)")
+ device_columns = {row["name"] for row in cur.fetchall()}
+ except Exception:
+ device_columns = set()
+
+ if device_columns and source_field not in device_columns:
+ mylog("debug", [f"[unlock_field] Source column {source_field} missing for {field_name}"])
+ return
# Unlock by resetting to empty (allows overwrite)
sql = f"UPDATE Devices SET {source_field}='' WHERE devMac = ?"
diff --git a/server/models/device_instance.py b/server/models/device_instance.py
index 90933e14..4ebb88be 100755
--- a/server/models/device_instance.py
+++ b/server/models/device_instance.py
@@ -593,7 +593,6 @@ class DeviceInstance:
conn = get_temp_db_connection()
cur = conn.cursor()
cur.execute(sql, values)
- conn.commit()
if data.get("createNew", False):
# Initialize source-tracking fields on device creation.
@@ -617,7 +616,6 @@ class DeviceInstance:
source_values.append(normalized_mac)
source_sql = f"UPDATE Devices SET {set_clause} WHERE devMac = ?"
cur.execute(source_sql, source_values)
- conn.commit()
# Enforce source tracking on user updates
# User-updated fields should have their *Source set to "USER"
@@ -631,6 +629,8 @@ class DeviceInstance:
conn.close()
return {"success": False, "error": f"Source tracking failed: {e}"}
+ # Commit all changes atomically after all operations succeed
+ conn.commit()
conn.close()
mylog("debug", f"[DeviceInstance] setDeviceData SQL: {sql.strip()}")
diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py
index f7bf4155..1e444816 100755
--- a/server/scan/device_handling.py
+++ b/server/scan/device_handling.py
@@ -545,12 +545,13 @@ def create_new_devices(db):
# Derive primary IP family values
cur_IP = str(cur_IP).strip() if cur_IP else ""
cur_IP_normalized = check_IP_format(cur_IP) if ":" not in cur_IP else cur_IP
-
- # Validate IPv6 addresses using format_ip_long for consistency
- if ":" in cur_IP_normalized:
+
+ # Validate IPv6 addresses using format_ip_long for consistency (do not store integer result)
+ if cur_IP_normalized and ":" in cur_IP_normalized:
validated_ipv6 = format_ip_long(cur_IP_normalized)
- cur_IP_normalized = validated_ipv6 if validated_ipv6 else ""
-
+ if validated_ipv6 is None or validated_ipv6 < 0:
+ cur_IP_normalized = ""
+
primary_ipv4 = cur_IP_normalized if cur_IP_normalized and ":" not in cur_IP_normalized else ""
primary_ipv6 = cur_IP_normalized if cur_IP_normalized and ":" in cur_IP_normalized else ""
diff --git a/test/authoritative_fields/test_device_field_lock.py b/test/authoritative_fields/test_device_field_lock.py
index b1361fcc..cd24cbfd 100644
--- a/test/authoritative_fields/test_device_field_lock.py
+++ b/test/authoritative_fields/test_device_field_lock.py
@@ -115,6 +115,30 @@ class TestDeviceFieldLock:
assert resp.status_code == 400
assert "cannot be locked" in resp.json.get("error", "")
+ def test_lock_field_normalizes_mac(self, client, test_mac, auth_headers):
+ """Lock endpoint should normalize MACs before applying locks."""
+ # Create device with normalized MAC
+ self.test_create_test_device(client, test_mac, auth_headers)
+
+ mac_variant = "aa-bb-cc-dd-ee-ff"
+ payload = {
+ "fieldName": "devName",
+ "lock": True
+ }
+ resp = client.post(
+ f"/device/{mac_variant}/field/lock",
+ json=payload,
+ headers=auth_headers
+ )
+ assert resp.status_code == 200, f"Failed to lock via normalized MAC: {resp.json}"
+ assert resp.json.get("locked") is True
+
+ # Verify source is LOCKED on normalized MAC
+ resp = client.get(f"/device/{test_mac}", headers=auth_headers)
+ assert resp.status_code == 200
+ device_data = resp.json
+ assert device_data.get("devNameSource") == "LOCKED"
+
def test_lock_all_tracked_fields(self, client, test_mac, auth_headers):
"""Lock each tracked field individually."""
# First create device
diff --git a/test/authoritative_fields/test_ip_format_and_locking.py b/test/authoritative_fields/test_ip_format_and_locking.py
index 3141a538..ff3d26a3 100644
--- a/test/authoritative_fields/test_ip_format_and_locking.py
+++ b/test/authoritative_fields/test_ip_format_and_locking.py
@@ -72,6 +72,103 @@ def ip_test_db():
conn.close()
+@pytest.fixture
+def new_device_db():
+ """Create an in-memory SQLite database for create_new_devices tests."""
+ conn = sqlite3.connect(":memory:")
+ conn.row_factory = sqlite3.Row
+ cur = conn.cursor()
+
+ cur.execute(
+ """
+ CREATE TABLE Devices (
+ devMac TEXT PRIMARY KEY,
+ devName TEXT,
+ devVendor TEXT,
+ devLastIP TEXT,
+ devPrimaryIPv4 TEXT,
+ devPrimaryIPv6 TEXT,
+ devFirstConnection TEXT,
+ devLastConnection TEXT,
+ devSyncHubNode TEXT,
+ devGUID TEXT,
+ devParentMAC TEXT,
+ devParentPort TEXT,
+ devSite TEXT,
+ devSSID TEXT,
+ devType TEXT,
+ devSourcePlugin TEXT,
+ devAlertEvents INTEGER,
+ devAlertDown INTEGER,
+ devPresentLastScan INTEGER,
+ devIsArchived INTEGER,
+ devIsNew INTEGER,
+ devSkipRepeated INTEGER,
+ devScan INTEGER,
+ devOwner TEXT,
+ devFavorite INTEGER,
+ devGroup TEXT,
+ devComments TEXT,
+ devLogEvents INTEGER,
+ devLocation TEXT,
+ devCustomProps TEXT,
+ devParentRelType TEXT,
+ devReqNicsOnline INTEGER
+ )
+ """
+ )
+
+ cur.execute(
+ """
+ CREATE TABLE CurrentScan (
+ cur_MAC TEXT,
+ cur_Name TEXT,
+ cur_Vendor TEXT,
+ cur_ScanMethod TEXT,
+ cur_IP TEXT,
+ cur_SyncHubNodeName TEXT,
+ cur_NetworkNodeMAC TEXT,
+ cur_PORT TEXT,
+ cur_NetworkSite TEXT,
+ cur_SSID TEXT,
+ cur_Type TEXT
+ )
+ """
+ )
+
+ cur.execute(
+ """
+ CREATE TABLE Events (
+ eve_MAC TEXT,
+ eve_IP TEXT,
+ eve_DateTime TEXT,
+ eve_EventType TEXT,
+ eve_AdditionalInfo TEXT,
+ eve_PendingAlertEmail INTEGER
+ )
+ """
+ )
+
+ cur.execute(
+ """
+ CREATE TABLE Sessions (
+ ses_MAC TEXT,
+ ses_IP TEXT,
+ ses_EventTypeConnection TEXT,
+ ses_DateTimeConnection TEXT,
+ ses_EventTypeDisconnection TEXT,
+ ses_DateTimeDisconnection TEXT,
+ ses_StillConnected INTEGER,
+ ses_AdditionalInfo TEXT
+ )
+ """
+ )
+
+ conn.commit()
+ yield conn
+ conn.close()
+
+
@pytest.fixture
def mock_ip_handlers():
"""Mock device_handling helper functions."""
@@ -311,6 +408,55 @@ def test_invalid_ip_values_rejected(ip_test_db, mock_ip_handlers):
), f"Invalid IP '{invalid_ip}' should not overwrite valid IPv4"
+def test_invalid_ipv6_rejected_on_create_new_devices(new_device_db):
+ """Invalid IPv6 values should not be persisted when creating new devices."""
+ cur = new_device_db.cursor()
+
+ cur.execute(
+ """
+ INSERT INTO CurrentScan (
+ cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP,
+ cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT,
+ cur_NetworkSite, cur_SSID, cur_Type
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """,
+ (
+ "AA:BB:CC:DD:EE:10",
+ "",
+ "Vendor",
+ "ARPSCAN",
+ "fe80::zz",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ),
+ )
+ new_device_db.commit()
+
+ db = Mock()
+ db.sql_connection = new_device_db
+ db.sql = cur
+ db.commitDB = Mock(side_effect=new_device_db.commit)
+
+ with patch("helper.get_setting_value", return_value=""), patch.object(
+ device_handling, "get_setting_value", return_value=""
+ ):
+ device_handling.create_new_devices(db)
+
+ row = cur.execute(
+ "SELECT devLastIP, devPrimaryIPv4, devPrimaryIPv6 FROM Devices WHERE devMac = ?",
+ ("AA:BB:CC:DD:EE:10",),
+ ).fetchone()
+
+ assert row is not None, "Device should be created"
+ assert row["devLastIP"] == "", "Invalid IPv6 should not set devLastIP"
+ assert row["devPrimaryIPv4"] == "", "Invalid IPv6 should not set devPrimaryIPv4"
+ assert row["devPrimaryIPv6"] == "", "Invalid IPv6 should not set devPrimaryIPv6"
+
+
def test_ipv4_ipv6_mixed_in_multiple_scans(ip_test_db, mock_ip_handlers):
"""Multiple scans with different IP types should set both primary fields correctly."""
cur = ip_test_db.cursor()
diff --git a/test/test_device_atomicity.py b/test/test_device_atomicity.py
new file mode 100644
index 00000000..39cbf2cd
--- /dev/null
+++ b/test/test_device_atomicity.py
@@ -0,0 +1,231 @@
+"""
+Test for atomicity of device updates with source-tracking.
+
+Verifies that:
+1. If source-tracking fails, the device row is rolled back.
+2. If source-tracking succeeds, device row and sources are both committed.
+3. Database remains consistent in both scenarios.
+"""
+
+import sys
+import os
+import sqlite3
+import tempfile
+import unittest
+from unittest.mock import patch
+
+
+# Add server and plugins to path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'server'))
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'front', 'plugins'))
+
+from models.device_instance import DeviceInstance # noqa: E402 [flake8 lint suppression]
+from plugin_helper import normalize_mac # noqa: E402 [flake8 lint suppression]
+
+
+class TestDeviceAtomicity(unittest.TestCase):
+ """Test atomic transactions for device updates with source-tracking."""
+
+ def setUp(self):
+ """Create an in-memory SQLite DB for testing."""
+ self.test_db = tempfile.NamedTemporaryFile(delete=False, suffix='.db')
+ self.test_db_path = self.test_db.name
+ self.test_db.close()
+
+ # Create minimal schema
+ conn = sqlite3.connect(self.test_db_path)
+ conn.row_factory = sqlite3.Row
+ cur = conn.cursor()
+
+ # Create Devices table with source-tracking columns
+ cur.execute("""
+ CREATE TABLE Devices (
+ devMac TEXT PRIMARY KEY,
+ devName TEXT,
+ devOwner TEXT,
+ devType TEXT,
+ devVendor TEXT,
+ devIcon TEXT,
+ devFavorite INTEGER DEFAULT 0,
+ devGroup TEXT,
+ devLocation TEXT,
+ devComments TEXT,
+ devParentMAC TEXT,
+ devParentPort TEXT,
+ devSSID TEXT,
+ devSite TEXT,
+ devStaticIP INTEGER DEFAULT 0,
+ devScan INTEGER DEFAULT 0,
+ devAlertEvents INTEGER DEFAULT 0,
+ devAlertDown INTEGER DEFAULT 0,
+ devParentRelType TEXT DEFAULT 'default',
+ devReqNicsOnline INTEGER DEFAULT 0,
+ devSkipRepeated INTEGER DEFAULT 0,
+ devIsNew INTEGER DEFAULT 0,
+ devIsArchived INTEGER DEFAULT 0,
+ devLastConnection TEXT,
+ devFirstConnection TEXT,
+ devLastIP TEXT,
+ devGUID TEXT,
+ devCustomProps TEXT,
+ devSourcePlugin TEXT,
+ devNameSource TEXT,
+ devTypeSource TEXT,
+ devVendorSource TEXT,
+ devIconSource TEXT,
+ devGroupSource TEXT,
+ devLocationSource TEXT,
+ devCommentsSource TEXT,
+ devMacSource TEXT
+ )
+ """)
+ conn.commit()
+ conn.close()
+
+ def tearDown(self):
+ """Clean up test database."""
+ if os.path.exists(self.test_db_path):
+ os.unlink(self.test_db_path)
+
+ def _get_test_db_connection(self):
+ """Override database connection for testing."""
+ conn = sqlite3.connect(self.test_db_path)
+ conn.row_factory = sqlite3.Row
+ return conn
+
+ def test_create_new_device_atomicity(self):
+ """
+ Test that device creation and source-tracking are atomic.
+ If source tracking fails, the device should not be created.
+ """
+ device_instance = DeviceInstance()
+ test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
+
+ # Patch at module level where it's used
+ with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
+ # Create a new device
+ data = {
+ "createNew": True,
+ "devMac": test_mac,
+ "devName": "Test Device",
+ "devOwner": "John Doe",
+ "devType": "Laptop",
+ }
+
+ result = device_instance.setDeviceData(test_mac, data)
+
+ # Verify success
+ self.assertTrue(result["success"], f"Device creation failed: {result}")
+
+ # Verify device exists
+ conn = self._get_test_db_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
+ device = cur.fetchone()
+ conn.close()
+
+ self.assertIsNotNone(device, "Device was not created")
+ self.assertEqual(device["devName"], "Test Device")
+
+ # Verify source tracking was set
+ self.assertEqual(device["devMacSource"], "NEWDEV")
+ self.assertEqual(device["devNameSource"], "NEWDEV")
+
+ def test_update_device_with_source_tracking_atomicity(self):
+ """
+ Test that device update and source-tracking are atomic.
+ If source tracking fails, the device update should be rolled back.
+ """
+ device_instance = DeviceInstance()
+ test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
+
+ # Create initial device
+ conn = self._get_test_db_connection()
+ cur = conn.cursor()
+ cur.execute("""
+ INSERT INTO Devices (
+ devMac, devName, devOwner, devType,
+ devNameSource, devTypeSource
+ ) VALUES (?, ?, ?, ?, ?, ?)
+ """, (test_mac, "Old Name", "Old Owner", "Desktop", "PLUGIN", "PLUGIN"))
+ conn.commit()
+ conn.close()
+
+ # Patch database connection
+ with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
+ with patch('models.device_instance.enforce_source_on_user_update') as mock_enforce:
+ mock_enforce.return_value = None
+ data = {
+ "createNew": False,
+ "devMac": test_mac,
+ "devName": "New Name",
+ "devOwner": "New Owner",
+ }
+
+ result = device_instance.setDeviceData(test_mac, data)
+
+ # Verify success
+ self.assertTrue(result["success"], f"Device update failed: {result}")
+
+ # Verify device was updated
+ conn = self._get_test_db_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
+ device = cur.fetchone()
+ conn.close()
+
+ self.assertEqual(device["devName"], "New Name")
+ self.assertEqual(device["devOwner"], "New Owner")
+
+ def test_source_tracking_failure_rolls_back_device(self):
+ """
+ Test that if enforce_source_on_user_update fails, the entire
+ transaction is rolled back (device and sources).
+ """
+ device_instance = DeviceInstance()
+ test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
+
+ # Create initial device
+ conn = self._get_test_db_connection()
+ cur = conn.cursor()
+ cur.execute("""
+ INSERT INTO Devices (
+ devMac, devName, devOwner, devType,
+ devNameSource, devTypeSource
+ ) VALUES (?, ?, ?, ?, ?, ?)
+ """, (test_mac, "Original Name", "Original Owner", "Desktop", "PLUGIN", "PLUGIN"))
+ conn.commit()
+ conn.close()
+
+ # Patch database connection and mock source enforcement failure
+ with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
+ with patch('models.device_instance.enforce_source_on_user_update') as mock_enforce:
+ # Simulate source tracking failure
+ mock_enforce.side_effect = Exception("Source tracking error")
+
+ data = {
+ "createNew": False,
+ "devMac": test_mac,
+ "devName": "Failed Update",
+ "devOwner": "Failed Owner",
+ }
+
+ result = device_instance.setDeviceData(test_mac, data)
+
+ # Verify error response
+ self.assertFalse(result["success"])
+ self.assertIn("Source tracking failed", result["error"])
+
+ # Verify device was NOT updated (rollback successful)
+ conn = self._get_test_db_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
+ device = cur.fetchone()
+ conn.close()
+
+ self.assertEqual(device["devName"], "Original Name", "Device should not have been updated on source tracking failure")
+ self.assertEqual(device["devOwner"], "Original Owner", "Device should not have been updated on source tracking failure")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_plugin_helper.py b/test/test_plugin_helper.py
index 1d712c21..9a88f39b 100644
--- a/test/test_plugin_helper.py
+++ b/test/test_plugin_helper.py
@@ -16,3 +16,9 @@ def test_normalize_mac_preserves_wildcard():
result = normalize_mac("aabbcc*")
assert result == "AA:BB:CC:*", f"Expected 'AA:BB:CC:*' but got '{result}'"
assert normalize_mac("aa:bb:cc:dd:ee:ff") == "AA:BB:CC:DD:EE:FF"
+
+
+def test_normalize_mac_preserves_internet_root():
+ assert normalize_mac("internet") == "Internet"
+ assert normalize_mac("Internet") == "Internet"
+ assert normalize_mac("INTERNET") == "Internet"