mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
feat: Implement forced device status updates and enhance related tests
This commit is contained in:
@@ -156,7 +156,7 @@ function getDeviceData() {
|
|||||||
},
|
},
|
||||||
// Group for other fields like static IP, archived status, etc.
|
// Group for other fields like static IP, archived status, etc.
|
||||||
DevDetail_DisplayFields_Title: {
|
DevDetail_DisplayFields_Title: {
|
||||||
data: ["devStaticIP", "devIsNew", "devFavorite", "devIsArchived"],
|
data: ["devStaticIP", "devIsNew", "devFavorite", "devIsArchived", "devForceStatus"],
|
||||||
docs: "https://docs.netalertx.com/DEVICE_DISPLAY_SETTINGS",
|
docs: "https://docs.netalertx.com/DEVICE_DISPLAY_SETTINGS",
|
||||||
iconClass: "fa fa-list-check",
|
iconClass: "fa fa-list-check",
|
||||||
inputGroupClasses: "field-group display-group col-lg-4 col-sm-6 col-xs-12",
|
inputGroupClasses: "field-group display-group col-lg-4 col-sm-6 col-xs-12",
|
||||||
@@ -295,8 +295,8 @@ function getDeviceData() {
|
|||||||
const currentSource = deviceData[sourceField] || "NEWDEV";
|
const currentSource = deviceData[sourceField] || "NEWDEV";
|
||||||
const sourceTitle = getString("FieldLock_Source_Label") + currentSource;
|
const sourceTitle = getString("FieldLock_Source_Label") + currentSource;
|
||||||
const sourceColor = currentSource === "USER" ? "text-warning" : (currentSource === "LOCKED" ? "text-danger" : "text-muted");
|
const sourceColor = currentSource === "USER" ? "text-warning" : (currentSource === "LOCKED" ? "text-danger" : "text-muted");
|
||||||
inlineControl += `<span class="input-group-addon ${sourceColor}" title="${sourceTitle}">
|
inlineControl += `<span class="input-group-addon pointer ${sourceColor}" title="${sourceTitle}">
|
||||||
<i class="fa-solid fa-tag"></i> ${currentSource}
|
${currentSource}
|
||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,14 +594,17 @@ function toggleFieldLock(mac, fieldName) {
|
|||||||
lockBtn.find("i").attr("class", `fa-solid ${lockIcon}`);
|
lockBtn.find("i").attr("class", `fa-solid ${lockIcon}`);
|
||||||
lockBtn.attr("title", lockTitle);
|
lockBtn.attr("title", lockTitle);
|
||||||
|
|
||||||
// Update source indicator if locked
|
// Update local source state
|
||||||
if (shouldLock) {
|
deviceData[sourceField] = shouldLock ? "LOCKED" : "";
|
||||||
const sourceIndicator = lockBtn.next();
|
|
||||||
if (sourceIndicator.hasClass("input-group-addon")) {
|
// Update source indicator
|
||||||
sourceIndicator.text("LOCKED");
|
const sourceIndicator = lockBtn.next();
|
||||||
sourceIndicator.attr("class", "input-group-addon text-danger");
|
if (sourceIndicator.hasClass("input-group-addon")) {
|
||||||
sourceIndicator.attr("title", getString("FieldLock_Source_Label") + "LOCKED");
|
const sourceValue = shouldLock ? "LOCKED" : "NEWDEV";
|
||||||
}
|
const sourceClass = shouldLock ? "input-group-addon text-danger" : "input-group-addon text-muted";
|
||||||
|
sourceIndicator.text(sourceValue);
|
||||||
|
sourceIndicator.attr("class", sourceClass);
|
||||||
|
sourceIndicator.attr("title", getString("FieldLock_Source_Label") + sourceValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage(shouldLock ? getString("FieldLock_Locked") : getString("FieldLock_Unlocked"), 3000, "modal_green");
|
showMessage(shouldLock ? getString("FieldLock_Locked") : getString("FieldLock_Unlocked"), 3000, "modal_green");
|
||||||
|
|||||||
@@ -1947,9 +1947,9 @@
|
|||||||
},
|
},
|
||||||
"default_value": "dont_force",
|
"default_value": "dont_force",
|
||||||
"options": [
|
"options": [
|
||||||
|
"dont_force" ,
|
||||||
"online",
|
"online",
|
||||||
"offline",
|
"offline"
|
||||||
"dont_force"
|
|
||||||
],
|
],
|
||||||
"localized": [
|
"localized": [
|
||||||
"name",
|
"name",
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ def update_devices_data_from_scan(db):
|
|||||||
# Update devPresentLastScan based on NICs presence
|
# Update devPresentLastScan based on NICs presence
|
||||||
update_devPresentLastScan_based_on_nics(db)
|
update_devPresentLastScan_based_on_nics(db)
|
||||||
|
|
||||||
|
# Force device status if configured
|
||||||
|
update_devPresentLastScan_based_on_force_status(db)
|
||||||
|
|
||||||
# Guess ICONS
|
# Guess ICONS
|
||||||
recordsToUpdate = []
|
recordsToUpdate = []
|
||||||
|
|
||||||
@@ -865,6 +868,72 @@ def update_devPresentLastScan_based_on_nics(db):
|
|||||||
return len(updates)
|
return len(updates)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------
|
||||||
|
# Force devPresentLastScan based on devForceStatus
|
||||||
|
def update_devPresentLastScan_based_on_force_status(db):
|
||||||
|
"""
|
||||||
|
Forces devPresentLastScan in the Devices table based on devForceStatus.
|
||||||
|
|
||||||
|
devForceStatus values:
|
||||||
|
- "online" -> devPresentLastScan = 1
|
||||||
|
- "offline" -> devPresentLastScan = 0
|
||||||
|
- "dont_force" or empty -> no change
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: A database object with `.execute()` and `.fetchone()` methods.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Number of devices updated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sql = db.sql
|
||||||
|
|
||||||
|
online_count_row = sql.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS cnt
|
||||||
|
FROM Devices
|
||||||
|
WHERE LOWER(COALESCE(devForceStatus, '')) = 'online'
|
||||||
|
AND devPresentLastScan != 1
|
||||||
|
"""
|
||||||
|
).fetchone()
|
||||||
|
online_updates = online_count_row["cnt"] if online_count_row else 0
|
||||||
|
|
||||||
|
offline_count_row = sql.execute(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) AS cnt
|
||||||
|
FROM Devices
|
||||||
|
WHERE LOWER(COALESCE(devForceStatus, '')) = 'offline'
|
||||||
|
AND devPresentLastScan != 0
|
||||||
|
"""
|
||||||
|
).fetchone()
|
||||||
|
offline_updates = offline_count_row["cnt"] if offline_count_row else 0
|
||||||
|
|
||||||
|
if online_updates > 0:
|
||||||
|
sql.execute(
|
||||||
|
"""
|
||||||
|
UPDATE Devices
|
||||||
|
SET devPresentLastScan = 1
|
||||||
|
WHERE LOWER(COALESCE(devForceStatus, '')) = 'online'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if offline_updates > 0:
|
||||||
|
sql.execute(
|
||||||
|
"""
|
||||||
|
UPDATE Devices
|
||||||
|
SET devPresentLastScan = 0
|
||||||
|
WHERE LOWER(COALESCE(devForceStatus, '')) = 'offline'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
total_updates = online_updates + offline_updates
|
||||||
|
if total_updates > 0:
|
||||||
|
mylog("debug", f"[Update Devices] Forced devPresentLastScan for {total_updates} devices")
|
||||||
|
|
||||||
|
db.commitDB()
|
||||||
|
return total_updates
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
# Check if the variable contains a valid MAC address or "Internet"
|
# Check if the variable contains a valid MAC address or "Internet"
|
||||||
def check_mac_or_internet(input_str):
|
def check_mac_or_internet(input_str):
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ def scan_db():
|
|||||||
devMac TEXT PRIMARY KEY,
|
devMac TEXT PRIMARY KEY,
|
||||||
devLastConnection TEXT,
|
devLastConnection TEXT,
|
||||||
devPresentLastScan INTEGER DEFAULT 0,
|
devPresentLastScan INTEGER DEFAULT 0,
|
||||||
|
devForceStatus TEXT,
|
||||||
devLastIP TEXT,
|
devLastIP TEXT,
|
||||||
devName TEXT,
|
devName TEXT,
|
||||||
devNameSource TEXT DEFAULT 'NEWDEV',
|
devNameSource TEXT DEFAULT 'NEWDEV',
|
||||||
@@ -93,6 +94,7 @@ def mock_device_handlers():
|
|||||||
with patch.multiple(
|
with patch.multiple(
|
||||||
device_handling,
|
device_handling,
|
||||||
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
|
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
|
||||||
|
update_devPresentLastScan_based_on_force_status=Mock(return_value=0),
|
||||||
query_MAC_vendor=Mock(return_value=-1),
|
query_MAC_vendor=Mock(return_value=-1),
|
||||||
guess_icon=Mock(return_value="icon"),
|
guess_icon=Mock(return_value="icon"),
|
||||||
guess_type=Mock(return_value="type"),
|
guess_type=Mock(return_value="type"),
|
||||||
|
|||||||
65
test/authoritative_fields/test_force_status.py
Normal file
65
test/authoritative_fields/test_force_status.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
"""Tests for forced device status updates."""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from server.scan import device_handling
|
||||||
|
|
||||||
|
|
||||||
|
class DummyDB:
|
||||||
|
"""Minimal DB wrapper compatible with device_handling helpers."""
|
||||||
|
|
||||||
|
def __init__(self, conn):
|
||||||
|
self.sql = conn.cursor()
|
||||||
|
self._conn = conn
|
||||||
|
|
||||||
|
def commitDB(self):
|
||||||
|
self._conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def test_force_status_updates_present_flag():
|
||||||
|
"""Forced status should override devPresentLastScan for online/offline values."""
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE Devices (
|
||||||
|
devMac TEXT PRIMARY KEY,
|
||||||
|
devPresentLastScan INTEGER,
|
||||||
|
devForceStatus TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
cur.executemany(
|
||||||
|
"""
|
||||||
|
INSERT INTO Devices (devMac, devPresentLastScan, devForceStatus)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
[
|
||||||
|
("AA:AA:AA:AA:AA:01", 0, "online"),
|
||||||
|
("AA:AA:AA:AA:AA:02", 1, "offline"),
|
||||||
|
("AA:AA:AA:AA:AA:03", 1, "dont_force"),
|
||||||
|
("AA:AA:AA:AA:AA:04", 0, None),
|
||||||
|
("AA:AA:AA:AA:AA:05", 0, "ONLINE"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
db = DummyDB(conn)
|
||||||
|
updated = device_handling.update_devPresentLastScan_based_on_force_status(db)
|
||||||
|
|
||||||
|
rows = {
|
||||||
|
row["devMac"]: row["devPresentLastScan"]
|
||||||
|
for row in cur.execute("SELECT devMac, devPresentLastScan FROM Devices")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert updated == 3
|
||||||
|
assert rows["AA:AA:AA:AA:AA:01"] == 1
|
||||||
|
assert rows["AA:AA:AA:AA:AA:02"] == 0
|
||||||
|
assert rows["AA:AA:AA:AA:AA:03"] == 1
|
||||||
|
assert rows["AA:AA:AA:AA:AA:04"] == 0
|
||||||
|
assert rows["AA:AA:AA:AA:AA:05"] == 1
|
||||||
|
|
||||||
|
conn.close()
|
||||||
@@ -29,6 +29,7 @@ def ip_test_db():
|
|||||||
devMac TEXT PRIMARY KEY,
|
devMac TEXT PRIMARY KEY,
|
||||||
devLastConnection TEXT,
|
devLastConnection TEXT,
|
||||||
devPresentLastScan INTEGER,
|
devPresentLastScan INTEGER,
|
||||||
|
devForceStatus TEXT,
|
||||||
devLastIP TEXT,
|
devLastIP TEXT,
|
||||||
devLastIpSource TEXT DEFAULT 'NEWDEV',
|
devLastIpSource TEXT DEFAULT 'NEWDEV',
|
||||||
devPrimaryIPv4 TEXT,
|
devPrimaryIPv4 TEXT,
|
||||||
@@ -78,6 +79,7 @@ def mock_ip_handlers():
|
|||||||
with patch.multiple(
|
with patch.multiple(
|
||||||
device_handling,
|
device_handling,
|
||||||
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
|
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
|
||||||
|
update_devPresentLastScan_based_on_force_status=Mock(return_value=0),
|
||||||
query_MAC_vendor=Mock(return_value=-1),
|
query_MAC_vendor=Mock(return_value=-1),
|
||||||
guess_icon=Mock(return_value="icon"),
|
guess_icon=Mock(return_value="icon"),
|
||||||
guess_type=Mock(return_value="type"),
|
guess_type=Mock(return_value="type"),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ def in_memory_db():
|
|||||||
devMac TEXT PRIMARY KEY,
|
devMac TEXT PRIMARY KEY,
|
||||||
devLastConnection TEXT,
|
devLastConnection TEXT,
|
||||||
devPresentLastScan INTEGER,
|
devPresentLastScan INTEGER,
|
||||||
|
devForceStatus TEXT,
|
||||||
devLastIP TEXT,
|
devLastIP TEXT,
|
||||||
devPrimaryIPv4 TEXT,
|
devPrimaryIPv4 TEXT,
|
||||||
devPrimaryIPv6 TEXT,
|
devPrimaryIPv6 TEXT,
|
||||||
@@ -69,6 +70,7 @@ def mock_device_handling():
|
|||||||
with patch.multiple(
|
with patch.multiple(
|
||||||
device_handling,
|
device_handling,
|
||||||
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
|
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
|
||||||
|
update_devPresentLastScan_based_on_force_status=Mock(return_value=0),
|
||||||
query_MAC_vendor=Mock(return_value=-1),
|
query_MAC_vendor=Mock(return_value=-1),
|
||||||
guess_icon=Mock(return_value="icon"),
|
guess_icon=Mock(return_value="icon"),
|
||||||
guess_type=Mock(return_value="type"),
|
guess_type=Mock(return_value="type"),
|
||||||
|
|||||||
Reference in New Issue
Block a user