Skip to content

Commit fcbe4ae

Browse files
committed
feat: Implement forced device status updates and enhance related tests
1 parent 9f1d04b commit fcbe4ae

File tree

7 files changed

+156
-13
lines changed

7 files changed

+156
-13
lines changed

front/deviceDetailsEdit.php

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function getDeviceData() {
156156
},
157157
// Group for other fields like static IP, archived status, etc.
158158
DevDetail_DisplayFields_Title: {
159-
data: ["devStaticIP", "devIsNew", "devFavorite", "devIsArchived"],
159+
data: ["devStaticIP", "devIsNew", "devFavorite", "devIsArchived", "devForceStatus"],
160160
docs: "https://docs.netalertx.com/DEVICE_DISPLAY_SETTINGS",
161161
iconClass: "fa fa-list-check",
162162
inputGroupClasses: "field-group display-group col-lg-4 col-sm-6 col-xs-12",
@@ -295,8 +295,8 @@ function getDeviceData() {
295295
const currentSource = deviceData[sourceField] || "NEWDEV";
296296
const sourceTitle = getString("FieldLock_Source_Label") + currentSource;
297297
const sourceColor = currentSource === "USER" ? "text-warning" : (currentSource === "LOCKED" ? "text-danger" : "text-muted");
298-
inlineControl += `<span class="input-group-addon ${sourceColor}" title="${sourceTitle}">
299-
<i class="fa-solid fa-tag"></i> ${currentSource}
298+
inlineControl += `<span class="input-group-addon pointer ${sourceColor}" title="${sourceTitle}">
299+
${currentSource}
300300
</span>`;
301301
}
302302

@@ -594,14 +594,17 @@ function toggleFieldLock(mac, fieldName) {
594594
lockBtn.find("i").attr("class", `fa-solid ${lockIcon}`);
595595
lockBtn.attr("title", lockTitle);
596596

597-
// Update source indicator if locked
598-
if (shouldLock) {
599-
const sourceIndicator = lockBtn.next();
600-
if (sourceIndicator.hasClass("input-group-addon")) {
601-
sourceIndicator.text("LOCKED");
602-
sourceIndicator.attr("class", "input-group-addon text-danger");
603-
sourceIndicator.attr("title", getString("FieldLock_Source_Label") + "LOCKED");
604-
}
597+
// Update local source state
598+
deviceData[sourceField] = shouldLock ? "LOCKED" : "";
599+
600+
// Update source indicator
601+
const sourceIndicator = lockBtn.next();
602+
if (sourceIndicator.hasClass("input-group-addon")) {
603+
const sourceValue = shouldLock ? "LOCKED" : "NEWDEV";
604+
const sourceClass = shouldLock ? "input-group-addon text-danger" : "input-group-addon text-muted";
605+
sourceIndicator.text(sourceValue);
606+
sourceIndicator.attr("class", sourceClass);
607+
sourceIndicator.attr("title", getString("FieldLock_Source_Label") + sourceValue);
605608
}
606609

607610
showMessage(shouldLock ? getString("FieldLock_Locked") : getString("FieldLock_Unlocked"), 3000, "modal_green");

front/plugins/newdev_template/config.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1947,9 +1947,9 @@
19471947
},
19481948
"default_value": "dont_force",
19491949
"options": [
1950+
"dont_force" ,
19501951
"online",
1951-
"offline",
1952-
"dont_force"
1952+
"offline"
19531953
],
19541954
"localized": [
19551955
"name",

server/scan/device_handling.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ def update_devices_data_from_scan(db):
243243
# Update devPresentLastScan based on NICs presence
244244
update_devPresentLastScan_based_on_nics(db)
245245

246+
# Force device status if configured
247+
update_devPresentLastScan_based_on_force_status(db)
248+
246249
# Guess ICONS
247250
recordsToUpdate = []
248251

@@ -865,6 +868,72 @@ def update_devPresentLastScan_based_on_nics(db):
865868
return len(updates)
866869

867870

871+
# -------------------------------------------------------------------------------
872+
# Force devPresentLastScan based on devForceStatus
873+
def update_devPresentLastScan_based_on_force_status(db):
874+
"""
875+
Forces devPresentLastScan in the Devices table based on devForceStatus.
876+
877+
devForceStatus values:
878+
- "online" -> devPresentLastScan = 1
879+
- "offline" -> devPresentLastScan = 0
880+
- "dont_force" or empty -> no change
881+
882+
Args:
883+
db: A database object with `.execute()` and `.fetchone()` methods.
884+
885+
Returns:
886+
int: Number of devices updated.
887+
"""
888+
889+
sql = db.sql
890+
891+
online_count_row = sql.execute(
892+
"""
893+
SELECT COUNT(*) AS cnt
894+
FROM Devices
895+
WHERE LOWER(COALESCE(devForceStatus, '')) = 'online'
896+
AND devPresentLastScan != 1
897+
"""
898+
).fetchone()
899+
online_updates = online_count_row["cnt"] if online_count_row else 0
900+
901+
offline_count_row = sql.execute(
902+
"""
903+
SELECT COUNT(*) AS cnt
904+
FROM Devices
905+
WHERE LOWER(COALESCE(devForceStatus, '')) = 'offline'
906+
AND devPresentLastScan != 0
907+
"""
908+
).fetchone()
909+
offline_updates = offline_count_row["cnt"] if offline_count_row else 0
910+
911+
if online_updates > 0:
912+
sql.execute(
913+
"""
914+
UPDATE Devices
915+
SET devPresentLastScan = 1
916+
WHERE LOWER(COALESCE(devForceStatus, '')) = 'online'
917+
"""
918+
)
919+
920+
if offline_updates > 0:
921+
sql.execute(
922+
"""
923+
UPDATE Devices
924+
SET devPresentLastScan = 0
925+
WHERE LOWER(COALESCE(devForceStatus, '')) = 'offline'
926+
"""
927+
)
928+
929+
total_updates = online_updates + offline_updates
930+
if total_updates > 0:
931+
mylog("debug", f"[Update Devices] Forced devPresentLastScan for {total_updates} devices")
932+
933+
db.commitDB()
934+
return total_updates
935+
936+
868937
# -------------------------------------------------------------------------------
869938
# Check if the variable contains a valid MAC address or "Internet"
870939
def check_mac_or_internet(input_str):

test/authoritative_fields/test_field_lock_scan_integration.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def scan_db():
3333
devMac TEXT PRIMARY KEY,
3434
devLastConnection TEXT,
3535
devPresentLastScan INTEGER DEFAULT 0,
36+
devForceStatus TEXT,
3637
devLastIP TEXT,
3738
devName TEXT,
3839
devNameSource TEXT DEFAULT 'NEWDEV',
@@ -93,6 +94,7 @@ def mock_device_handlers():
9394
with patch.multiple(
9495
device_handling,
9596
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
97+
update_devPresentLastScan_based_on_force_status=Mock(return_value=0),
9698
query_MAC_vendor=Mock(return_value=-1),
9799
guess_icon=Mock(return_value="icon"),
98100
guess_type=Mock(return_value="type"),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Tests for forced device status updates."""
2+
3+
import sqlite3
4+
5+
from server.scan import device_handling
6+
7+
8+
class DummyDB:
9+
"""Minimal DB wrapper compatible with device_handling helpers."""
10+
11+
def __init__(self, conn):
12+
self.sql = conn.cursor()
13+
self._conn = conn
14+
15+
def commitDB(self):
16+
self._conn.commit()
17+
18+
19+
def test_force_status_updates_present_flag():
20+
"""Forced status should override devPresentLastScan for online/offline values."""
21+
conn = sqlite3.connect(":memory:")
22+
conn.row_factory = sqlite3.Row
23+
cur = conn.cursor()
24+
25+
cur.execute(
26+
"""
27+
CREATE TABLE Devices (
28+
devMac TEXT PRIMARY KEY,
29+
devPresentLastScan INTEGER,
30+
devForceStatus TEXT
31+
)
32+
"""
33+
)
34+
35+
cur.executemany(
36+
"""
37+
INSERT INTO Devices (devMac, devPresentLastScan, devForceStatus)
38+
VALUES (?, ?, ?)
39+
""",
40+
[
41+
("AA:AA:AA:AA:AA:01", 0, "online"),
42+
("AA:AA:AA:AA:AA:02", 1, "offline"),
43+
("AA:AA:AA:AA:AA:03", 1, "dont_force"),
44+
("AA:AA:AA:AA:AA:04", 0, None),
45+
("AA:AA:AA:AA:AA:05", 0, "ONLINE"),
46+
],
47+
)
48+
conn.commit()
49+
50+
db = DummyDB(conn)
51+
updated = device_handling.update_devPresentLastScan_based_on_force_status(db)
52+
53+
rows = {
54+
row["devMac"]: row["devPresentLastScan"]
55+
for row in cur.execute("SELECT devMac, devPresentLastScan FROM Devices")
56+
}
57+
58+
assert updated == 3
59+
assert rows["AA:AA:AA:AA:AA:01"] == 1
60+
assert rows["AA:AA:AA:AA:AA:02"] == 0
61+
assert rows["AA:AA:AA:AA:AA:03"] == 1
62+
assert rows["AA:AA:AA:AA:AA:04"] == 0
63+
assert rows["AA:AA:AA:AA:AA:05"] == 1
64+
65+
conn.close()

test/authoritative_fields/test_ip_format_and_locking.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def ip_test_db():
2929
devMac TEXT PRIMARY KEY,
3030
devLastConnection TEXT,
3131
devPresentLastScan INTEGER,
32+
devForceStatus TEXT,
3233
devLastIP TEXT,
3334
devLastIpSource TEXT DEFAULT 'NEWDEV',
3435
devPrimaryIPv4 TEXT,
@@ -78,6 +79,7 @@ def mock_ip_handlers():
7879
with patch.multiple(
7980
device_handling,
8081
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
82+
update_devPresentLastScan_based_on_force_status=Mock(return_value=0),
8183
query_MAC_vendor=Mock(return_value=-1),
8284
guess_icon=Mock(return_value="icon"),
8385
guess_type=Mock(return_value="type"),

test/authoritative_fields/test_ip_update_logic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def in_memory_db():
2323
devMac TEXT PRIMARY KEY,
2424
devLastConnection TEXT,
2525
devPresentLastScan INTEGER,
26+
devForceStatus TEXT,
2627
devLastIP TEXT,
2728
devPrimaryIPv4 TEXT,
2829
devPrimaryIPv6 TEXT,
@@ -69,6 +70,7 @@ def mock_device_handling():
6970
with patch.multiple(
7071
device_handling,
7172
update_devPresentLastScan_based_on_nics=Mock(return_value=0),
73+
update_devPresentLastScan_based_on_force_status=Mock(return_value=0),
7274
query_MAC_vendor=Mock(return_value=-1),
7375
guess_icon=Mock(return_value="icon"),
7476
guess_type=Mock(return_value="type"),

0 commit comments

Comments
 (0)