name resolution config clean up, authoritative fields fixes for none values, css fixes

Signed-off-by: jokob-sk <jokob.sk@gmail.com>
This commit is contained in:
jokob-sk
2026-02-03 20:40:11 +11:00
parent 8a5d3b1548
commit 81202ce07e
11 changed files with 294 additions and 79 deletions

View File

@@ -2249,9 +2249,8 @@ textarea[readonly],
.red-hover-border:hover .red-hover-border:hover
{ {
border-color: red !important; border-color: #ff0000 !important;
border-width: 1px; box-shadow: #ff0000;
border-style: solid;
} }
@@ -2260,12 +2259,6 @@ textarea[readonly],
background-color: red !important; background-color: red !important;
} }
#multi-edit-form .form-group
{
height: 45px;
}
.top-left-logo .top-left-logo
{ {
height:35px; height:35px;

View File

@@ -59,22 +59,22 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="col-md-12"> <div class="col-md-12">
<div class="col-md-2" style=""> <div class="col-md-4" style="">
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red" id="btnDeleteMAC" onclick="askDeleteSelectedDevices()"><?= lang('Maintenance_Tool_del_selecteddev');?></button> <button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red col-md-12 col-sm-12 col-xs-12" id="btnDeleteMAC" onclick="askDeleteSelectedDevices()"><?= lang('Maintenance_Tool_del_selecteddev');?></button>
</div> </div>
<div class="col-md-10"><?= lang('Maintenance_Tool_del_selecteddev_text');?></div> <div class="col-md-8"><?= lang('Maintenance_Tool_del_selecteddev_text');?></div>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<div class="col-md-2" style=""> <div class="col-md-4" style="">
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red" id="btnUnlockFieldsSelected" onclick="askUnlockFieldsSelected()"><?= lang('Maintenance_Tool_unlockFields_selecteddev');?></button> <button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red col-md-12 col-sm-12 col-xs-12" id="btnUnlockFieldsSelected" onclick="askUnlockFieldsSelected()"><?= lang('Maintenance_Tool_unlockFields_selecteddev');?></button>
</div> </div>
<div class="col-md-10"><?= lang('Maintenance_Tool_del_unlockFields_selecteddev_text');?></div> <div class="col-md-8"><?= lang('Maintenance_Tool_del_unlockFields_selecteddev_text');?></div>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<div class="col-md-2" style=""> <div class="col-md-4" style="">
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red" id="btnClearSourceFields" onclick="askClearSourceFields()"><?= lang('Maintenance_Tool_clearSourceFields_selected');?></button> <button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red col-md-12 col-sm-12 col-xs-12" id="btnClearSourceFields" onclick="askClearSourceFields()"><?= lang('Maintenance_Tool_clearSourceFields_selected');?></button>
</div> </div>
<div class="col-md-10"><?= lang('Maintenance_Tool_clearSourceFields_selected_text');?></div> <div class="col-md-8"><?= lang('Maintenance_Tool_clearSourceFields_selected_text');?></div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -225,6 +225,68 @@
"string": "Maximum time per device scan in seconds to wait for the script to finish. If this time is exceeded the script is aborted." "string": "Maximum time per device scan 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": ["devFQDN"],
"options": [
"devFQDN",
"devName"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Set always columns"
}
],
"description": [
{
"language_code": "en_us",
"string": "These columns are treated as authoritative and will overwrite existing values, including those set by other plugins, unless the current value was explicitly set by the user (<code>Source = USER</code> or <code>Source = LOCKED</code>)."
}
]
},
{
"function": "SET_EMPTY",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
},
"default_value": [],
"options": [
"devFQDN",
"devName"
],
"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 (<code>NULL</code> / empty string) or if their Source is set to <code>NEWDEV</code>"
}
]
} }
], ],
"database_column_definitions": [ "database_column_definitions": [

View File

@@ -233,6 +233,68 @@
"string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen." "string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen."
} }
] ]
},
{
"function": "SET_ALWAYS",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
},
"default_value": ["devFQDN"],
"options": [
"devFQDN",
"devName"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Set always columns"
}
],
"description": [
{
"language_code": "en_us",
"string": "These columns are treated as authoritative and will overwrite existing values, including those set by other plugins, unless the current value was explicitly set by the user (<code>Source = USER</code> or <code>Source = LOCKED</code>)."
}
]
},
{
"function": "SET_EMPTY",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
},
"default_value": [],
"options": [
"devFQDN",
"devName"
],
"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 (<code>NULL</code> / empty string) or if their Source is set to <code>NEWDEV</code>"
}
]
} }
], ],
"database_column_definitions": [ "database_column_definitions": [

View File

@@ -233,6 +233,68 @@
"string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen." "string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen."
} }
] ]
},
{
"function": "SET_ALWAYS",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
},
"default_value": ["devFQDN"],
"options": [
"devFQDN",
"devName"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Set always columns"
}
],
"description": [
{
"language_code": "en_us",
"string": "These columns are treated as authoritative and will overwrite existing values, including those set by other plugins, unless the current value was explicitly set by the user (<code>Source = USER</code> or <code>Source = LOCKED</code>)."
}
]
},
{
"function": "SET_EMPTY",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
},
"default_value": [],
"options": [
"devFQDN",
"devName"
],
"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 (<code>NULL</code> / empty string) or if their Source is set to <code>NEWDEV</code>"
}
]
} }
], ],
"database_column_definitions": [ "database_column_definitions": [

View File

@@ -233,6 +233,68 @@
"string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen." "string": "Maximale Zeit in Sekunden, die auf den Abschluss des Skripts gewartet werden soll. Bei Überschreitung dieser Zeit wird das Skript abgebrochen."
} }
] ]
},
{
"function": "SET_ALWAYS",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
"transformers": []
}
]
},
"default_value": ["devFQDN"],
"options": [
"devFQDN",
"devName"
],
"localized": ["name", "description"],
"name": [
{
"language_code": "en_us",
"string": "Set always columns"
}
],
"description": [
{
"language_code": "en_us",
"string": "These columns are treated as authoritative and will overwrite existing values, including those set by other plugins, unless the current value was explicitly set by the user (<code>Source = USER</code> or <code>Source = LOCKED</code>)."
}
]
},
{
"function": "SET_EMPTY",
"type": {
"dataType": "array",
"elements": [
{
"elementType": "select",
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
"transformers": []
}
]
},
"default_value": [],
"options": [
"devFQDN",
"devName"
],
"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 (<code>NULL</code> / empty string) or if their Source is set to <code>NEWDEV</code>"
}
]
} }
], ],
"database_column_definitions": [ "database_column_definitions": [

View File

@@ -208,27 +208,6 @@ class DB:
# Init the AppEvent database table # Init the AppEvent database table
AppEvent_obj(self) AppEvent_obj(self)
# # -------------------------------------------------------------------------------
# def get_table_as_json(self, sqlQuery):
# # mylog('debug',[ '[Database] - get_table_as_json - Query: ', sqlQuery])
# try:
# self.sql.execute(sqlQuery)
# columnNames = list(map(lambda x: x[0], self.sql.description))
# rows = self.sql.fetchall()
# except sqlite3.Error as e:
# mylog('verbose',[ '[Database] - SQL ERROR: ', e])
# return json_obj({}, []) # return empty object
# result = {"data":[]}
# for row in rows:
# tmp = row_to_json(columnNames, row)
# result["data"].append(tmp)
# # mylog('debug',[ '[Database] - get_table_as_json - returning ', len(rows), " rows with columns: ", columnNames])
# # mylog('debug',[ '[Database] - get_table_as_json - returning json ', json.dumps(result) ])
# return json_obj(result, columnNames)
def get_table_as_json(self, sqlQuery, parameters=None): def get_table_as_json(self, sqlQuery, parameters=None):
""" """
Wrapper to use the central get_table_as_json helper. Wrapper to use the central get_table_as_json helper.

View File

@@ -96,35 +96,37 @@ def can_overwrite_field(field_name, current_value, current_source, plugin_prefix
bool: True if overwrite allowed. bool: True if overwrite allowed.
""" """
empty_values = ("0.0.0.0", "", "null", "(unknown)", "(name not found)", None)
# Rule 1: USER/LOCKED protected # Rule 1: USER/LOCKED protected
if current_source in ("USER", "LOCKED"): if current_source in ("USER", "LOCKED"):
return False return False
# Rule 2: Must provide a non-empty value or same as current # Rule 2: Must provide a non-empty value, otherwise reject
empty_values = ("0.0.0.0", "", "null", "(unknown)", "(name not found)", None) if (not field_value) or (field_value in empty_values) or ((isinstance(field_value, str) and not field_value.strip())):
if not field_value or (isinstance(field_value, str) and not field_value.strip()):
if current_value == field_value:
return True # Allow overwrite if value same
return False return False
# Rule 3: SET_ALWAYS # Rule 3: Allow overwrite if value same to update Source fields
if current_value == field_value:
return True
# Rule 4: SET_ALWAYS
set_always = plugin_settings.get("set_always", []) set_always = plugin_settings.get("set_always", [])
if field_name in set_always: if field_name in set_always:
return True return True
# Rule 4: SET_EMPTY # Rule 5: SET_EMPTY
set_empty = plugin_settings.get("set_empty", []) set_empty = plugin_settings.get("set_empty", [])
empty_values = ("0.0.0.0", "", "null", "(unknown)", "(name not found)", None)
if field_name in set_empty: if field_name in set_empty:
if current_value in empty_values: if current_value in empty_values:
return True return True
return False return False
# Rule 5: Default - overwrite if current value empty # Rule 6: Default - overwrite if current value empty
if current_value in empty_values: if current_value in empty_values:
return True return True
# Rule 6: Optional override flag to allow overwrite if value changed (devLastIP) # Rule 7: Optional override flag to allow overwrite if value changed (devLastIP)
if allow_override_if_changed and field_value != current_value: if allow_override_if_changed and field_value != current_value:
return True return True

View File

@@ -295,6 +295,7 @@ def ensure_CurrentScan(sql) -> bool:
scanVlan STRING(250), scanVlan STRING(250),
scanParentMAC STRING(250), scanParentMAC STRING(250),
scanParentPort STRING(250), scanParentPort STRING(250),
scanFQDN STRING(250),
scanType STRING(250) scanType STRING(250)
); );
""") """)

View File

@@ -102,17 +102,6 @@ FIELD_SPECS = {
"priority": ["NSLOOKUP", "AVAHISCAN", "NBTSCAN", "DIGSCAN", "ARPSCAN", "DHCPLSS", "NEWDEV", "N/A"], "priority": ["NSLOOKUP", "AVAHISCAN", "NBTSCAN", "DIGSCAN", "ARPSCAN", "DHCPLSS", "NEWDEV", "N/A"],
}, },
# ==========================================================
# DEVICE FQDN
# ==========================================================
"devFQDN": {
"scan_col": "scanName",
"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,
},
# ========================================================== # ==========================================================
# IP ADDRESS (last seen) # IP ADDRESS (last seen)
# ========================================================== # ==========================================================
@@ -297,8 +286,6 @@ def update_devices_data_from_scan(db):
current_source = row_dict.get(f"{field}Source") or "" current_source = row_dict.get(f"{field}Source") or ""
new_value = row_dict.get(scan_col) new_value = row_dict.get(scan_col)
mylog("debug", f"[Update Devices] - current_value: {current_value} new_value: {new_value} -> {field}")
if can_overwrite_field( if can_overwrite_field(
field_name=field, field_name=field,
current_value=current_value, current_value=current_value,
@@ -326,6 +313,7 @@ def update_devices_data_from_scan(db):
WHERE devMac = ? WHERE devMac = ?
""" """
mylog("debug", f"[Update Devices] - ({source_prefix}) current_value: {current_value} new_value: {new_value} -> {field}")
mylog("debug", f"[Update Devices] - ({source_prefix}) {spec['scan_col']} -> {field}") mylog("debug", f"[Update Devices] - ({source_prefix}) {spec['scan_col']} -> {field}")
mylog("debug", f"[Update Devices] sql_tmp: {sql_tmp}, sql_val: {sql_val}") mylog("debug", f"[Update Devices] sql_tmp: {sql_tmp}, sql_val: {sql_val}")
sql.execute(sql_tmp, sql_val) sql.execute(sql_tmp, sql_val)
@@ -978,34 +966,38 @@ def update_devices_names(pm):
notFound = 0 notFound = 0
for device in devices: for device in devices:
newName = nameNotFound resolved_successfully = False
newFQDN = ""
# Attempt each resolution strategy in order # Attempt each resolution strategy in order
for resolve_fn, label in strategies: for resolve_fn, label in strategies:
resolved = resolve_fn(device["devMac"], device["devLastIP"]) resolved = resolve_fn(device["devMac"], device["devLastIP"])
# Only use name if resolving both name and FQDN # Extract values
newName = resolved.cleaned if resolve_both_name_and_fqdn else None current_raw = resolved.raw if resolved.raw else ""
newFQDN = resolved.raw current_cleaned = resolved.cleaned if resolved.cleaned else ""
# Validation: Ensure we actually got a real FQDN/Name
if (current_raw not in [nameNotFound, "", "localhost."] and " communications error to " not in current_raw):
# If a valid result is found, record it and stop further attempts
if (
newFQDN not in [nameNotFound, "", "localhost."] and " communications error to " not in newFQDN
):
foundStats[label] += 1 foundStats[label] += 1
resolved_successfully = True
if resolve_both_name_and_fqdn: if resolve_both_name_and_fqdn:
recordsToUpdate.append([newName, label, newFQDN, label, device["devMac"]]) # Logic: If cleaned name is missing, fallback to raw,
# but if both are missing, this strategy failed.
final_name = current_cleaned if current_cleaned else current_raw
recordsToUpdate.append([final_name, label, current_raw, label, device["devMac"]])
else: else:
recordsToUpdate.append([newFQDN, label, device["devMac"]]) recordsToUpdate.append([current_raw, label, device["devMac"]])
break
# If no name was resolved, queue device for "(name not found)" update break # Stop trying other strategies for this device
if resolve_both_name_and_fqdn and newName == nameNotFound:
# If after all strategies we found nothing
if not resolved_successfully:
notFound += 1 notFound += 1
if device["devName"] != nameNotFound: if resolve_both_name_and_fqdn:
recordsNotFound.append([nameNotFound, device["devMac"]]) if device["devName"] != nameNotFound:
recordsNotFound.append([nameNotFound, device["devMac"]])
return recordsToUpdate, recordsNotFound, foundStats, notFound return recordsToUpdate, recordsNotFound, foundStats, notFound

View File

@@ -103,7 +103,7 @@ def process_scan(db):
# Clear current scan as processed # Clear current scan as processed
# 🐛 CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes # 🐛 CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes
db.sql.execute("DELETE FROM CurrentScan") # db.sql.execute("DELETE FROM CurrentScan")
# re-broadcast unread notifiation count to update FE # re-broadcast unread notifiation count to update FE
update_unread_notifications_count() update_unread_notifications_count()