From fbed99b33801b487d3f7618613b32f38583cb661 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Tue, 15 Jul 2025 11:45:07 +0200 Subject: [PATCH 01/35] Add static DHCP lease editing table fully synced with the advanced textarea below Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 183 ++++++++++++++++++++++++++++++++++++ settings-dhcp.lp | 21 ++++- style/pi-hole.css | 3 + 3 files changed, 206 insertions(+), 1 deletion(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 370396cdd4..f4576bcc28 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -212,6 +212,8 @@ function delLease(ip) { function fillDHCPhosts(data) { $("#dhcp-hosts").val(data.value.join("\n")); + // Trigger input to update the table + $("#dhcp-hosts").trigger("input"); } function processDHCPConfig() { @@ -227,6 +229,187 @@ function processDHCPConfig() { }); } +function parseStaticDHCPLine(line) { + // Accepts: [hwaddr][,ipaddr][,hostname] (all optional, comma-separated, no advanced tokens) + // Returns null if advanced/invalid, or {hwaddr, ipaddr, hostname} + + // If the line is empty, return an object with empty fields + if (!line.trim()) + return { + hwaddr: "", + ipaddr: "", + hostname: "", + }; + + // Advanced if contains id:, set:, tag:, ignore, [ or ] + if (/id:|set:|tag:|ignore|\[|]|lease_time|,\s*,/.test(line)) return "advanced"; + + // Split the line by commas and trim whitespace + const parts = line.split(",").map(s => s.trim()); + + // If there are more than 3 parts, it's considered advanced + if (parts.length > 3) return "advanced"; + + // Check if first part is a valid MAC address + const macRegex = /^(?:[\da-f]{2}:){5}[\da-f]{2}$/i; + const haveMAC = parts.length > 0 && macRegex.test(parts[0]); + const hwaddr = haveMAC ? parts[0] : ""; + // Check if the first or second part is a valid IP address + const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}$/; + let ipaddr = ""; + if (ipRegex.test(parts[0])) ipaddr = parts[0]; + else if (parts.length > 1 && ipRegex.test(parts[1])) ipaddr = parts[1]; + const haveIP = ipaddr.length > 0; + // Check if the second or third part is a valid hostname + let hostname = ""; + if (parts.length > 2 && parts[2].length > 0) hostname = parts[2]; + else if (parts.length > 1 && parts[1].length > 0 && (!haveIP || !haveMAC)) hostname = parts[1]; + + return { + hwaddr, + ipaddr, + hostname, + }; +} + +function updateTextareaFromTable() { + const lines = []; + $("#StaticDHCPTable tbody tr").each(function () { + const row = $(this); + // Skip advanced pseudo-rows + if (row.hasClass("table-warning")) { + lines.push(row.data("original-line") || ""); + return; + } + + const hwaddr = row.find(".static-hwaddr").text().trim(); + const ipaddr = row.find(".static-ipaddr").text().trim(); + const hostname = row.find(".static-hostname").text().trim(); + // Only add if at least one field is non-empty + if (hwaddr || ipaddr || hostname) { + lines.push([hwaddr, ipaddr, hostname].filter(Boolean).join(",")); + } + }); + $("#dhcp-hosts").val(lines.join("\n")).trigger("input"); +} + +// Save button for each row updates only that line in the textarea +$(document).on("click", ".save-static-row", function () { + const rowIdx = Number.parseInt($(this).data("row"), 10); + const row = $(this).closest("tr"); + const hwaddr = row.find(".static-hwaddr").text().trim(); + const ipaddr = row.find(".static-ipaddr").text().trim(); + const hostname = row.find(".static-hostname").text().trim(); + const lines = $("#dhcp-hosts").val().split(/\r?\n/); + // Only update if at least one field is non-empty + lines[rowIdx] = + hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; + $("#dhcp-hosts").val(lines.join("\n")); + // Optionally, re-render the table to reflect changes + renderStaticDHCPTable(); +}); + +// Delete button for each row removes that line from the textarea and updates the table +$(document).on("click", ".delete-static-row", function () { + const rowIdx = Number.parseInt($(this).data("row"), 10); + const lines = $("#dhcp-hosts").val().split(/\r?\n/); + lines.splice(rowIdx, 1); + $("#dhcp-hosts").val(lines.join("\n")); + renderStaticDHCPTable(); +}); + +// Add button for each row inserts a new empty line after this row +$(document).on("click", ".add-static-row", function () { + const rowIdx = Number.parseInt($(this).data("row"), 10); + const lines = $("#dhcp-hosts").val().split(/\r?\n/); + lines.splice(rowIdx + 1, 0, ""); + $("#dhcp-hosts").val(lines.join("\n")); + renderStaticDHCPTable(); + // Focus the new row after render + setTimeout(() => { + $("#StaticDHCPTable tbody tr") + .eq(rowIdx + 1) + .find("td:first") + .focus(); + }, 10); +}); + +// Update table on load and whenever textarea changes $(() => { processDHCPConfig(); + renderStaticDHCPTable(); + $("#dhcp-hosts").on("input", renderStaticDHCPTable); +}); + +// When editing a cell, disable all action buttons except the save button in the current row +$(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function () { + const row = $(this).closest("tr"); + // Disable all action buttons in all rows + $( + "#StaticDHCPTable .save-static-row, #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" + ).prop("disabled", true); + // Enable only the save button in the current row + row.find(".save-static-row").prop("disabled", false); + // Show a hint below the current row if not already present + if (!row.next().hasClass("edit-hint-row")) { + row.next(".edit-hint-row").remove(); // Remove any existing hint + row.after( + 'Please save this line before editing another or leaving the page, otherwise your changes will be lost.' + ); + } }); +// On save, re-enable all buttons and remove the hint +$(document).on("click", ".save-static-row", function () { + $( + "#StaticDHCPTable .save-static-row, #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" + ).prop("disabled", false); + $(".edit-hint-row").remove(); +}); +// On table redraw, ensure all buttons are enabled and hints are removed +function renderStaticDHCPTable() { + const tbody = $("#StaticDHCPTable tbody"); + tbody.empty(); + const lines = $("#dhcp-hosts").val().split(/\r?\n/); + let rowCount = 0; + for (const [idx, line] of lines.entries()) { + const parsed = parseStaticDHCPLine(line); + if (parsed === "advanced") { + const tr = $( + 'Advanced settings present in line ' + + (idx + 1) + + "" + ); + tr.data("original-line", line); + tbody.append(tr); + continue; + } + + rowCount++; + const tr = $( + "" + + '' + + '' + + '' + + "" + + ' ' + + ' ' + + '' + + "" + + "" + ); + // Set cell values, with placeholder for empty hwaddr + tr.find(".static-hwaddr").text(parsed.hwaddr); + tr.find(".static-ipaddr").text(parsed.ipaddr); + tr.find(".static-hostname").text(parsed.hostname); + tbody.append(tr); + } + + tbody.find(".save-static-row, .delete-static-row, .add-static-row").prop("disabled", false); + tbody.find(".edit-hint-row").remove(); +} diff --git a/settings-dhcp.lp b/settings-dhcp.lp index 6ee2f8dfa1..b94aa8dfb0 100644 --- a/settings-dhcp.lp +++ b/settings-dhcp.lp @@ -171,10 +171,29 @@ mg.include('scripts/lua/settings_header.lp','r')

Static DHCP configuration

+
+
+
+ + + + + + + + + + + + +
MAC addressIP addressHostnameActions
+
+
+

Specify per host parameters for the DHCP server. This allows a machine with a particular hardware address to be always allocated the same hostname, IP address and lease time. A hostname specified like this overrides any supplied by the DHCP client on the machine. It is also allowable to omit the hardware address and include the hostname, in which case the IP address and lease times will apply to any machine claiming that name.

-
  +

Each entry should be on a separate line, and should be of the form:

[<hwaddr>][,id:<client_id>|*][,set:<tag>][,tag:<tag>][,<ipaddr>][,<hostname>][,<lease_time>][,ignore]

Only one entry per MAC address is allowed.

diff --git a/style/pi-hole.css b/style/pi-hole.css index fc51792e94..d9261d074e 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1396,6 +1396,9 @@ table.dataTable tbody > tr > .selected { .mb-3 { margin-bottom: 1rem !important; } +.mb-4 { + margin-bottom: 2rem !important; +} .ml-3 { margin-left: 1rem !important; } From 2495a8e175cac862cc1abfeb5068be5da318513a Mon Sep 17 00:00:00 2001 From: DL6ER Date: Tue, 15 Jul 2025 12:30:08 +0200 Subject: [PATCH 02/35] Add copy from current to static DHCP leases button Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index f4576bcc28..4e901f0b12 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -79,9 +79,14 @@ $(() => { data.ip + '" data-del-ip="' + data.ip + - '">' + - '' + - ""; + '">' + + ' '; $("td:eq(6)", row).html(button); }, select: { @@ -413,3 +418,14 @@ function renderStaticDHCPTable() { tbody.find(".save-static-row, .delete-static-row, .add-static-row").prop("disabled", false); tbody.find(".edit-hint-row").remove(); } + +// Copy button for each lease row copies the lease as a new static lease line +$(document).on("click", ".copy-to-static", function () { + const hwaddr = $(this).data("hwaddr") || ""; + const ip = $(this).data("ip") || ""; + const hostname = $(this).data("hostname") || ""; + const line = [hwaddr, ip, hostname].filter(Boolean).join(","); + const textarea = $("#dhcp-hosts"); + const val = textarea.val(); + textarea.val(val ? val + "\n" + line : line).trigger("input"); +}); From 937cd047343af7568f354355a12151294bbde338 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Tue, 15 Jul 2025 12:37:09 +0200 Subject: [PATCH 03/35] Add tooltips to the action buttons Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 4e901f0b12..907a653ba0 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -79,14 +79,14 @@ $(() => { data.ip + '" data-del-ip="' + data.ip + - '">' + + '" title="Delete lease" data-toggle="tooltip">' + ' '; + '" title="Copy to static leases" data-toggle="tooltip">'; $("td:eq(6)", row).html(button); }, select: { @@ -398,13 +398,13 @@ function renderStaticDHCPTable() { "" + ' ' + + '" title="Save changes to this line" data-toggle="tooltip"> ' + ' ' + + '" title="Delete this line" data-toggle="tooltip"> ' + '' + + '" title="Add new line after this" data-toggle="tooltip">' + "" + "" ); From 416468f233ba55ef1289e8d6b63b42110ffd0448 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Tue, 15 Jul 2025 12:48:22 +0200 Subject: [PATCH 04/35] Remove unused code Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 907a653ba0..5e4e065523 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -277,27 +277,6 @@ function parseStaticDHCPLine(line) { }; } -function updateTextareaFromTable() { - const lines = []; - $("#StaticDHCPTable tbody tr").each(function () { - const row = $(this); - // Skip advanced pseudo-rows - if (row.hasClass("table-warning")) { - lines.push(row.data("original-line") || ""); - return; - } - - const hwaddr = row.find(".static-hwaddr").text().trim(); - const ipaddr = row.find(".static-ipaddr").text().trim(); - const hostname = row.find(".static-hostname").text().trim(); - // Only add if at least one field is non-empty - if (hwaddr || ipaddr || hostname) { - lines.push([hwaddr, ipaddr, hostname].filter(Boolean).join(",")); - } - }); - $("#dhcp-hosts").val(lines.join("\n")).trigger("input"); -} - // Save button for each row updates only that line in the textarea $(document).on("click", ".save-static-row", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); @@ -375,7 +354,6 @@ function renderStaticDHCPTable() { const tbody = $("#StaticDHCPTable tbody"); tbody.empty(); const lines = $("#dhcp-hosts").val().split(/\r?\n/); - let rowCount = 0; for (const [idx, line] of lines.entries()) { const parsed = parseStaticDHCPLine(line); if (parsed === "advanced") { @@ -389,7 +367,6 @@ function renderStaticDHCPTable() { continue; } - rowCount++; const tr = $( "" + '' + From f1ddb23825f52c019b279842a9d31ddb2e83882a Mon Sep 17 00:00:00 2001 From: DL6ER Date: Tue, 15 Jul 2025 13:01:34 +0200 Subject: [PATCH 05/35] Bind data attributes instead of inlining them for security (to prevent any possibility for code injection) Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 82 ++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 5e4e065523..8f1b15ea59 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -74,20 +74,23 @@ $(() => { }, rowCallback(row, data) { $(row).attr("data-id", data.ip); - const button = - '' + - ' '; - $("td:eq(6)", row).html(button); + // Create buttons without data-* attributes in HTML + const $deleteBtn = $( + '' + ) + .attr("id", "deleteLease_" + data.ip) + .attr("data-del-ip", data.ip) + .attr("title", "Delete lease") + .attr("data-toggle", "tooltip"); + const $copyBtn = $( + '' + ) + .attr("title", "Copy to static leases") + .attr("data-toggle", "tooltip") + .data("hwaddr", data.hwaddr || "") + .data("ip", data.ip || "") + .data("hostname", data.name || ""); + $("td:eq(6)", row).empty().append($deleteBtn, " ", $copyBtn); }, select: { style: "multi", @@ -367,24 +370,39 @@ function renderStaticDHCPTable() { continue; } - const tr = $( - "" + - '' + - '' + - '' + - "" + - ' ' + - ' ' + - '' + - "" + - "" - ); + const tr = $("") + .append($('')) + .append($('')) + .append($('')) + .append( + $("") + .append( + $( + '' + ) + .attr("data-row", idx) + .attr("title", "Save changes to this line") + .attr("data-toggle", "tooltip") + ) + .append(" ") + .append( + $( + '' + ) + .attr("data-row", idx) + .attr("title", "Delete this line") + .attr("data-toggle", "tooltip") + ) + .append(" ") + .append( + $( + '' + ) + .attr("data-row", idx) + .attr("title", "Add new line after this") + .attr("data-toggle", "tooltip") + ) + ); // Set cell values, with placeholder for empty hwaddr tr.find(".static-hwaddr").text(parsed.hwaddr); tr.find(".static-ipaddr").text(parsed.ipaddr); From 320c8d9973d76c925bb861b0c89ccbc248871c0e Mon Sep 17 00:00:00 2001 From: DL6ER Date: Tue, 15 Jul 2025 16:00:09 +0200 Subject: [PATCH 06/35] Allow "-" as separator for MAC addresses Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 8f1b15ea59..534e761193 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -259,7 +259,7 @@ function parseStaticDHCPLine(line) { if (parts.length > 3) return "advanced"; // Check if first part is a valid MAC address - const macRegex = /^(?:[\da-f]{2}:){5}[\da-f]{2}$/i; + const macRegex = /^(?:[\da-f]{2}[:-]){5}[\da-f]{2}$/i; const haveMAC = parts.length > 0 && macRegex.test(parts[0]); const hwaddr = haveMAC ? parts[0] : ""; // Check if the first or second part is a valid IP address From 69b461ae8ee88b98e0676d367910d667d17c96c6 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Tue, 15 Jul 2025 20:07:16 +0200 Subject: [PATCH 07/35] Add IPv6 support for static DHCP lease management Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 20 +++++++++++++------- scripts/js/utils.js | 16 ++++++++++++++++ settings-dhcp.lp | 2 +- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 534e761193..ac77320507 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -249,8 +249,8 @@ function parseStaticDHCPLine(line) { hostname: "", }; - // Advanced if contains id:, set:, tag:, ignore, [ or ] - if (/id:|set:|tag:|ignore|\[|]|lease_time|,\s*,/.test(line)) return "advanced"; + // Advanced if contains id:, set:, tag:, ignore + if (/id:|set:|tag:|ignore|lease_time|,\s*,/.test(line)) return "advanced"; // Split the line by commas and trim whitespace const parts = line.split(",").map(s => s.trim()); @@ -262,12 +262,18 @@ function parseStaticDHCPLine(line) { const macRegex = /^(?:[\da-f]{2}[:-]){5}[\da-f]{2}$/i; const haveMAC = parts.length > 0 && macRegex.test(parts[0]); const hwaddr = haveMAC ? parts[0] : ""; - // Check if the first or second part is a valid IP address - const ipRegex = /^(?:\d{1,3}\.){3}\d{1,3}$/; - let ipaddr = ""; - if (ipRegex.test(parts[0])) ipaddr = parts[0]; - else if (parts.length > 1 && ipRegex.test(parts[1])) ipaddr = parts[1]; + + // Check if the first or second part is a valid IPv4 or IPv6 address + const hasSquareBrackets0 = parts[0][0] === "[" && parts[0].at(-1) === "]"; + const ipv60 = hasSquareBrackets0 ? parts[0].slice(1, -1) : parts[0]; + const hasSquareBrackets1 = parts.length > 1 && parts[1][0] === "[" && parts[1].at(-1) === "]"; + const ipv61 = hasSquareBrackets1 ? parts[1].slice(1, -1) : parts.length > 1 ? parts[1] : ""; + const firstIsValidIP = utils.validateIPv4(parts[0]) || utils.validateIPv6(ipv60); + const secondIsValidIP = + parts.length > 1 && (utils.validateIPv4(parts[1]) || utils.validateIPv6(ipv61)); + const ipaddr = firstIsValidIP ? parts[0] : secondIsValidIP ? parts[1] : ""; const haveIP = ipaddr.length > 0; + // Check if the second or third part is a valid hostname let hostname = ""; if (parts.length > 2 && parts[2].length > 0) hostname = parts[2]; diff --git a/scripts/js/utils.js b/scripts/js/utils.js index e063168e73..a358f37c0c 100644 --- a/scripts/js/utils.js +++ b/scripts/js/utils.js @@ -201,6 +201,13 @@ function validateIPv4CIDR(ip) { return ipv4validator.test(ip); } +function validateIPv4(ip) { + // Add pseudo-CIDR to the IPv4 + const ipv4WithCIDR = ip.includes("/") ? ip : ip + "/32"; + // Validate the IPv4/CIDR + return validateIPv4CIDR(ipv4WithCIDR); +} + // Pi-hole IPv6/CIDR validator by DL6ER, see regexr.com/50csn function validateIPv6CIDR(ip) { // One IPv6 element is 16bit: 0000 - FFFF @@ -216,6 +223,13 @@ function validateIPv6CIDR(ip) { return ipv6validator.test(ip); } +function validateIPv6(ip) { + // Add pseudo-CIDR to the IPv6 + const ipv6WithCIDR = ip.includes("/") ? ip : ip + "/128"; + // Validate the IPv6/CIDR + return validateIPv6CIDR(ipv6WithCIDR); +} + function validateMAC(mac) { const macvalidator = /^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/; return macvalidator.test(mac); @@ -682,7 +696,9 @@ globalThis.utils = (function () { disableAll, enableAll, validateIPv4CIDR, + validateIPv4, validateIPv6CIDR, + validateIPv6, setBsSelectDefaults, stateSaveCallback, stateLoadCallback, diff --git a/settings-dhcp.lp b/settings-dhcp.lp index b94aa8dfb0..5c4af11c10 100644 --- a/settings-dhcp.lp +++ b/settings-dhcp.lp @@ -196,7 +196,7 @@ mg.include('scripts/lua/settings_header.lp','r')

Each entry should be on a separate line, and should be of the form:

[<hwaddr>][,id:<client_id>|*][,set:<tag>][,tag:<tag>][,<ipaddr>][,<hostname>][,<lease_time>][,ignore]
-

Only one entry per MAC address is allowed.

+

Only one entry per MAC address is allowed. IPv6 addresses must be enclosed in square brackets like [2001:db8::1].

Examples:

From e7d70559c3b2c9920c0c5cc6f435d8ce0331af53 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Wed, 16 Jul 2025 08:28:51 +0200 Subject: [PATCH 08/35] Add line numbers to dhcp.hosts textarea Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 22 ++++++++++++++++++++++ settings-dhcp.lp | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index ac77320507..b60e05cca7 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -430,3 +430,25 @@ $(document).on("click", ".copy-to-static", function () { const val = textarea.val(); textarea.val(val ? val + "\n" + line : line).trigger("input"); }); + +// Add line numbers to the textarea for static DHCP hosts +document.addEventListener("DOMContentLoaded", function () { + const textarea = document.getElementById("dhcp-hosts"); + const linesElem = document.getElementById("dhcp-hosts-lines"); + function updateLineNumbers() { + const lines = textarea.value.split("\n").length || 1; + linesElem.innerHTML = Array.from({ length: lines }, function (_, i) { + return i + 1; + }).join("
"); + } + + function syncScroll() { + linesElem.scrollTop = textarea.scrollTop; + } + + if (textarea && linesElem) { + textarea.addEventListener("input", updateLineNumbers); + textarea.addEventListener("scroll", syncScroll); + updateLineNumbers(); + } +}); diff --git a/settings-dhcp.lp b/settings-dhcp.lp index 5c4af11c10..5d8780eaa9 100644 --- a/settings-dhcp.lp +++ b/settings-dhcp.lp @@ -193,7 +193,10 @@ mg.include('scripts/lua/settings_header.lp','r')

Specify per host parameters for the DHCP server. This allows a machine with a particular hardware address to be always allocated the same hostname, IP address and lease time. A hostname specified like this overrides any supplied by the DHCP client on the machine. It is also allowable to omit the hardware address and include the hostname, in which case the IP address and lease times will apply to any machine claiming that name.

- +
+ + +

Each entry should be on a separate line, and should be of the form:

[<hwaddr>][,id:<client_id>|*][,set:<tag>][,tag:<tag>][,<ipaddr>][,<hostname>][,<lease_time>][,ignore]

Only one entry per MAC address is allowed. IPv6 addresses must be enclosed in square brackets like [2001:db8::1].

From 3489ddf28449437cbad7f96a221168c947066b14 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Sun, 20 Jul 2025 20:33:35 +0200 Subject: [PATCH 09/35] Validate hwaddr and IP address while typing Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 38 +++++++++++++++++++++++++++++++------ scripts/js/utils.js | 8 +++++--- style/pi-hole.css | 10 ++++++++++ 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index b60e05cca7..dfe5ac0193 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -259,9 +259,8 @@ function parseStaticDHCPLine(line) { if (parts.length > 3) return "advanced"; // Check if first part is a valid MAC address - const macRegex = /^(?:[\da-f]{2}[:-]){5}[\da-f]{2}$/i; - const haveMAC = parts.length > 0 && macRegex.test(parts[0]); - const hwaddr = haveMAC ? parts[0] : ""; + const haveMAC = parts.length > 0 && utils.validateMAC(parts[0]); + const hwaddr = haveMAC ? parts[0].trim() : ""; // Check if the first or second part is a valid IPv4 or IPv6 address const hasSquareBrackets0 = parts[0][0] === "[" && parts[0].at(-1) === "]"; @@ -271,13 +270,14 @@ function parseStaticDHCPLine(line) { const firstIsValidIP = utils.validateIPv4(parts[0]) || utils.validateIPv6(ipv60); const secondIsValidIP = parts.length > 1 && (utils.validateIPv4(parts[1]) || utils.validateIPv6(ipv61)); - const ipaddr = firstIsValidIP ? parts[0] : secondIsValidIP ? parts[1] : ""; + const ipaddr = firstIsValidIP ? parts[0].trim() : secondIsValidIP ? parts[1].trim() : ""; const haveIP = ipaddr.length > 0; // Check if the second or third part is a valid hostname let hostname = ""; - if (parts.length > 2 && parts[2].length > 0) hostname = parts[2]; - else if (parts.length > 1 && parts[1].length > 0 && (!haveIP || !haveMAC)) hostname = parts[1]; + if (parts.length > 2 && parts[2].length > 0) hostname = parts[2].trim(); + else if (parts.length > 1 && parts[1].length > 0 && (!haveIP || !haveMAC)) + hostname = parts[1].trim(); return { hwaddr, @@ -452,3 +452,29 @@ document.addEventListener("DOMContentLoaded", function () { updateLineNumbers(); } }); + +$(document).on("input blur paste", "#StaticDHCPTable td.static-hwaddr", function () { + const val = $(this).text().trim(); + if (val && !utils.validateMAC(val)) { + $(this).addClass("table-danger"); + $(this).removeClass("table-success"); + $(this).attr("title", "Invalid MAC address format"); + } else { + $(this).addClass("table-success"); + $(this).removeClass("table-danger"); + $(this).attr("title", ""); + } +}); + +$(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function () { + const val = $(this).text().trim(); + if (val && !(utils.validateIPv4(val) || utils.validateIPv6(val))) { + $(this).addClass("table-danger"); + $(this).removeClass("table-success"); + $(this).attr("title", "Invalid IP address format"); + } else { + $(this).addClass("table-success"); + $(this).removeClass("table-danger"); + $(this).attr("title", ""); + } +}); diff --git a/scripts/js/utils.js b/scripts/js/utils.js index a358f37c0c..b5b6f306c5 100644 --- a/scripts/js/utils.js +++ b/scripts/js/utils.js @@ -231,13 +231,15 @@ function validateIPv6(ip) { } function validateMAC(mac) { - const macvalidator = /^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$/; - return macvalidator.test(mac); + // Format: xx:xx:xx:xx:xx:xx where each xx is 0-9 or a-f (case insensitive) + // Also allows dashes as separator, e.g. xx-xx-xx-xx-xx-xx + const macvalidator = /^([\da-f]{2}[:-]){5}([\da-f]{2})$/i; + return macvalidator.test(mac.trim()); } function validateHostname(name) { const namevalidator = /[^<>;"]/; - return namevalidator.test(name); + return namevalidator.test(name.trim()); } // set bootstrap-select defaults diff --git a/style/pi-hole.css b/style/pi-hole.css index d9261d074e..c760685bad 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1612,3 +1612,13 @@ textarea.field-sizing-content { opacity: 0; transition: opacity 200ms ease-in-out; } + +#StaticDHCPTable td.table-danger { + background-color: #ff000022 !important; + color: #fff !important; +} + +#StaticDHCPTable td.table-success { + background-color: #74c70022 !important; + color: #fff !important; +} From 9959791c31da8e147d8a3758048226aa7fdf629b Mon Sep 17 00:00:00 2001 From: DL6ER Date: Sun, 20 Jul 2025 20:40:24 +0200 Subject: [PATCH 10/35] Reject saving if there are still validation errors in the table Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index dfe5ac0193..932c10aebd 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -293,6 +293,20 @@ $(document).on("click", ".save-static-row", function () { const hwaddr = row.find(".static-hwaddr").text().trim(); const ipaddr = row.find(".static-ipaddr").text().trim(); const hostname = row.find(".static-hostname").text().trim(); + + // Validate MAC and IP before saving + const macValid = !hwaddr || utils.validateMAC(hwaddr); + const ipValid = !ipaddr || utils.validateIPv4(ipaddr) || utils.validateIPv6(ipaddr); + if (!macValid || !ipValid) { + utils.showAlert( + "error", + "fa-times", + "Cannot save: Invalid MAC or IP address", + "Please correct the highlighted fields before saving." + ); + return; + } + const lines = $("#dhcp-hosts").val().split(/\r?\n/); // Only update if at least one field is non-empty lines[rowIdx] = From 7a90e561827ea1e3b14df98833fb0832b30269be Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Tue, 22 Jul 2025 16:17:51 -0300 Subject: [PATCH 11/35] Use background colors consistent with other tables on high contrast themes Signed-off-by: RD WebDesign --- style/themes/high-contrast-dark.css | 3 +++ style/themes/high-contrast.css | 3 +++ 2 files changed, 6 insertions(+) diff --git a/style/themes/high-contrast-dark.css b/style/themes/high-contrast-dark.css index 530fedf56e..8b4547cbe1 100644 --- a/style/themes/high-contrast-dark.css +++ b/style/themes/high-contrast-dark.css @@ -911,9 +911,12 @@ input[type="password"]::-webkit-caps-lock-indicator { } /* Tables and Datatables */ +#StaticDHCPTable .table-danger, .blocked-row td { background-color: rgba(56, 0, 40, 0.35); } + +#StaticDHCPTable .table-success, .allowed-row td { background-color: rgba(0, 64, 64, 0.35); } diff --git a/style/themes/high-contrast.css b/style/themes/high-contrast.css index b9467ed404..032b13b69c 100644 --- a/style/themes/high-contrast.css +++ b/style/themes/high-contrast.css @@ -386,9 +386,12 @@ select:-webkit-autofill { background-color: #fcfcfc; } +#StaticDHCPTable .table-danger, .blocked-row td { background-color: rgba(230, 160, 160, 0.3); } + +#StaticDHCPTable .table-success, .allowed-row td { background-color: rgba(236, 255, 246, 0.3); } From 05595e35c94719c5a5082420c6cb568f6008e082 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Wed, 23 Jul 2025 20:49:28 +0200 Subject: [PATCH 12/35] Use non-transparent colors in the static DHCP table Signed-off-by: DL6ER --- style/pi-hole.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/style/pi-hole.css b/style/pi-hole.css index c760685bad..09c7e9034b 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1614,11 +1614,11 @@ textarea.field-sizing-content { } #StaticDHCPTable td.table-danger { - background-color: #ff000022 !important; + background-color: #700000 !important; color: #fff !important; } #StaticDHCPTable td.table-success { - background-color: #74c70022 !important; + background-color: #599900 !important; color: #fff !important; } From ce2a4501c6aa26bd4f38042834b72be518e55c57 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Thu, 24 Jul 2025 06:21:34 +0200 Subject: [PATCH 13/35] Better synchronize lines numbers and textarea Signed-off-by: DL6ER --- scripts/js/settings-dhcp.js | 42 ++++++++++++++++++++++++++++++------- settings-dhcp.lp | 6 +++--- style/pi-hole.css | 26 +++++++++++++++++++++++ 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 932c10aebd..3074b97c06 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -255,8 +255,8 @@ function parseStaticDHCPLine(line) { // Split the line by commas and trim whitespace const parts = line.split(",").map(s => s.trim()); - // If there are more than 3 parts, it's considered advanced - if (parts.length > 3) return "advanced"; + // If there are more than 3 parts or less than 2, it's considered advanced + if (parts.length > 3 || parts.length < 2) return "advanced"; // Check if first part is a valid MAC address const haveMAC = parts.length > 0 && utils.validateMAC(parts[0]); @@ -449,11 +449,31 @@ $(document).on("click", ".copy-to-static", function () { document.addEventListener("DOMContentLoaded", function () { const textarea = document.getElementById("dhcp-hosts"); const linesElem = document.getElementById("dhcp-hosts-lines"); - function updateLineNumbers() { + let lastLineCount = 0; + + function updateLineNumbers(force) { + if (!textarea || !linesElem) return; const lines = textarea.value.split("\n").length || 1; - linesElem.innerHTML = Array.from({ length: lines }, function (_, i) { - return i + 1; - }).join("
"); + if (!force && lines === lastLineCount) return; + lastLineCount = lines; + let html = ""; + for (let i = 1; i <= lines; i++) html += i + "
"; + linesElem.innerHTML = html; + // Apply the same styles to the lines element as the textarea + for (const property of [ + "fontFamily", + "fontSize", + "fontWeight", + "letterSpacing", + "lineHeight", + "padding", + "height", + ]) { + linesElem.style[property] = globalThis.getComputedStyle(textarea)[property]; + } + + // Match height and scroll + linesElem.style.height = textarea.offsetHeight > 0 ? textarea.offsetHeight + "px" : "auto"; } function syncScroll() { @@ -461,9 +481,15 @@ document.addEventListener("DOMContentLoaded", function () { } if (textarea && linesElem) { - textarea.addEventListener("input", updateLineNumbers); + textarea.addEventListener("input", function () { + updateLineNumbers(false); + }); textarea.addEventListener("scroll", syncScroll); - updateLineNumbers(); + window.addEventListener("resize", function () { + updateLineNumbers(true); + }); + updateLineNumbers(true); + syncScroll(); } }); diff --git a/settings-dhcp.lp b/settings-dhcp.lp index 5d8780eaa9..6a1deba156 100644 --- a/settings-dhcp.lp +++ b/settings-dhcp.lp @@ -193,9 +193,9 @@ mg.include('scripts/lua/settings_header.lp','r')

Specify per host parameters for the DHCP server. This allows a machine with a particular hardware address to be always allocated the same hostname, IP address and lease time. A hostname specified like this overrides any supplied by the DHCP client on the machine. It is also allowable to omit the hardware address and include the hostname, in which case the IP address and lease times will apply to any machine claiming that name.

-
- - +
+ +

Each entry should be on a separate line, and should be of the form:

[<hwaddr>][,id:<client_id>|*][,set:<tag>][,tag:<tag>][,<ipaddr>][,<hostname>][,<lease_time>][,ignore]
diff --git a/style/pi-hole.css b/style/pi-hole.css index 09c7e9034b..f3430ccb1e 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1589,6 +1589,32 @@ textarea.field-sizing-content { field-sizing: content; } +textarea.no-wrap { + white-space: pre; + overflow-x: auto; + overflow-y: auto; + resize: none; +} + +div.line-numbers { + user-select: none; + text-align: right; + color: #aaa; + border-right: 1px solid #ddd; + padding: 8px 4px 8px 0; + font-family: monospace; + font-size: 13px; + min-width: 2em; + overflow-x: hidden; + overflow-y: hidden; +} + +div.dhcp-hosts-wrapper { + position: relative; + display: flex; + gap: 0.5em; +} + /* Used in interfaces page */ .list-group-item { background: transparent; From e160fa64c72a34f64b35f4531d259996b3dc3594 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Thu, 24 Jul 2025 16:40:34 +0200 Subject: [PATCH 14/35] Remove !important Signed-off-by: DL6ER --- style/pi-hole.css | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/style/pi-hole.css b/style/pi-hole.css index f3430ccb1e..6acd5c4b35 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1597,6 +1597,8 @@ textarea.no-wrap { } div.line-numbers { + -webkit-user-select: none; + -moz-user-select: none; user-select: none; text-align: right; color: #aaa; @@ -1640,11 +1642,11 @@ div.dhcp-hosts-wrapper { } #StaticDHCPTable td.table-danger { - background-color: #700000 !important; - color: #fff !important; + background-color: #700000; + color: #fff; } #StaticDHCPTable td.table-success { - background-color: #599900 !important; - color: #fff !important; + background-color: #599900; + color: #fff; } From bd173da6b7d417376f2084e5714b5e279595bc9c Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Thu, 26 Mar 2026 23:02:04 -0300 Subject: [PATCH 15/35] Fix regex for hostname validation in DHCP settings When using the `v` flag, hyphens need to be escaped inside a character class. Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 016d080d63..74ae32d132 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -522,7 +522,7 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function $(document).on("input blur paste", "#StaticDHCPTable td.static-hostname", function () { const val = $(this).text().trim(); // Hostnames must not contain spaces, commas, or characters invalid in DNS names - const hostnameValidator = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/v; + const hostnameValidator = /^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$/v; if (val && !hostnameValidator.test(val)) { $(this).addClass("table-danger"); $(this).removeClass("table-success"); From a23462fd2c07ad17ce4d5c02aaa9e49f3d2c425f Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Fri, 27 Mar 2026 01:53:07 -0300 Subject: [PATCH 16/35] Add buttons to lines containing "advanced" settings Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 103 +++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 74ae32d132..8d369ba2e3 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -286,8 +286,8 @@ function parseStaticDHCPLine(line) { }; } -// Save button for each row updates only that line in the textarea -$(document).on("click", ".save-static-row", function () { +// Save button for each row updates only that line in the textarea, if it doesn't contain the class ".disabled" +$(document).on("click", ".save-static-row:not(.disabled)", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); const row = $(this).closest("tr"); const hwaddr = row.find(".static-hwaddr").text().trim(); @@ -365,13 +365,15 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function ( ); } }); -// On save, re-enable all buttons and remove the hint + +// On save, re-enable all buttons (except buttons with class "disabled") and remove the hint $(document).on("click", ".save-static-row", function () { $( - "#StaticDHCPTable .save-static-row, #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" + "#StaticDHCPTable .save-static-row:not(.disabled), #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" ).prop("disabled", false); $(".edit-hint-row").remove(); }); + // On table redraw, ensure all buttons are enabled and hints are removed function renderStaticDHCPTable() { const tbody = $("#StaticDHCPTable tbody"); @@ -379,54 +381,59 @@ function renderStaticDHCPTable() { const lines = $("#dhcp-hosts").val().split(/\r?\n/); for (const [idx, line] of lines.entries()) { const parsed = parseStaticDHCPLine(line); + + const saveBtn = $( + '' + ) + .attr("data-row", idx) + .attr("title", "Confirm changes to this line") + .attr("data-toggle", "tooltip"); + + const delBtn = $( + '' + ) + .attr("data-row", idx) + .attr("title", "Delete this line") + .attr("data-toggle", "tooltip"); + + const addBtn = $( + '' + ) + .attr("data-row", idx) + .attr("title", "Add new line after this") + .attr("data-toggle", "tooltip"); + + const tr = $("") + if (parsed === "advanced") { - const tr = $( - 'Advanced settings present in line ' + - (idx + 1) + - "" - ); + tr.addClass("table-warning") + .append( + 'Advanced settings present in line ' + + (idx + 1) + + "", + ) + + // Keep the original data tr.data("original-line", line); - tbody.append(tr); - continue; + + // Disable the save button on advanced rows + saveBtn + .addClass("disabled") + .prop("disabled", true) + .attr("title", "Disabled"); + + } else { + // Append 3 cells containing parsed values, with placeholder for empty hwaddr + tr.append($('').text(parsed.hwaddr)) + .append($('').text(parsed.ipaddr)) + .append( + $('').text(parsed.hostname), + ); } - const tr = $("") - .append($('')) - .append($('')) - .append($('')) - .append( - $("") - .append( - $( - '' - ) - .attr("data-row", idx) - .attr("title", "Confirm changes to this line") - .attr("data-toggle", "tooltip") - ) - .append(" ") - .append( - $( - '' - ) - .attr("data-row", idx) - .attr("title", "Delete this line") - .attr("data-toggle", "tooltip") - ) - .append(" ") - .append( - $( - '' - ) - .attr("data-row", idx) - .attr("title", "Add new line after this") - .attr("data-toggle", "tooltip") - ) - ); - // Set cell values, with placeholder for empty hwaddr - tr.find(".static-hwaddr").text(parsed.hwaddr); - tr.find(".static-ipaddr").text(parsed.ipaddr); - tr.find(".static-hostname").text(parsed.hostname); + // Append a last cell containing the buttons + tr.append($("").append(saveBtn, " ", delBtn, " ", addBtn)); + tbody.append(tr); } From 02f143aa97b32743650bdb66912ddeab15ec6da2 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Fri, 27 Mar 2026 01:58:23 -0300 Subject: [PATCH 17/35] Make sure the .save-static-row button looks disabled when needed Signed-off-by: RD WebDesign --- style/pi-hole.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/style/pi-hole.css b/style/pi-hole.css index 97d93925b5..3365479710 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1645,6 +1645,10 @@ div.dhcp-hosts-wrapper { color: #fff; } +.save-static-row.disabled { + opacity: 0.15; +} + /* Used in query log page */ td.dnssec { padding-inline-start: 2.25em !important; From 6d50dc552e7e4eaccf7dc24b6d32ae86df85214d Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Sun, 29 Mar 2026 11:23:39 -0300 Subject: [PATCH 18/35] Fix prettier complaints after merging development Fix regex adding `v` flag and fix line breaks to respect maximum lenght Note: these issues were not previously reported because there was a previous merge conflict. The prettier test was executed only after the merge conflict was resolved. Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 8d369ba2e3..86c3c53851 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -250,7 +250,7 @@ function parseStaticDHCPLine(line) { }; // Advanced if contains id:, set:, tag:, ignore - if (/id:|set:|tag:|ignore|lease_time|,\s*,/.test(line)) return "advanced"; + if (/id:|set:|tag:|ignore|lease_time|,\s*,/v.test(line)) return "advanced"; // Split the line by commas and trim whitespace const parts = line.split(",").map(s => s.trim()); @@ -307,7 +307,7 @@ $(document).on("click", ".save-static-row:not(.disabled)", function () { return; } - const lines = $("#dhcp-hosts").val().split(/\r?\n/); + const lines = $("#dhcp-hosts").val().split(/\r?\n/v); // Only update if at least one field is non-empty lines[rowIdx] = hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; @@ -319,7 +319,7 @@ $(document).on("click", ".save-static-row:not(.disabled)", function () { // Delete button for each row removes that line from the textarea and updates the table $(document).on("click", ".delete-static-row", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); - const lines = $("#dhcp-hosts").val().split(/\r?\n/); + const lines = $("#dhcp-hosts").val().split(/\r?\n/v); lines.splice(rowIdx, 1); $("#dhcp-hosts").val(lines.join("\n")); renderStaticDHCPTable(); @@ -328,7 +328,7 @@ $(document).on("click", ".delete-static-row", function () { // Add button for each row inserts a new empty line after this row $(document).on("click", ".add-static-row", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); - const lines = $("#dhcp-hosts").val().split(/\r?\n/); + const lines = $("#dhcp-hosts").val().split(/\r?\n/v); lines.splice(rowIdx + 1, 0, ""); $("#dhcp-hosts").val(lines.join("\n")); renderStaticDHCPTable(); @@ -378,7 +378,7 @@ $(document).on("click", ".save-static-row", function () { function renderStaticDHCPTable() { const tbody = $("#StaticDHCPTable tbody"); tbody.empty(); - const lines = $("#dhcp-hosts").val().split(/\r?\n/); + const lines = $("#dhcp-hosts").val().split(/\r?\n/v); for (const [idx, line] of lines.entries()) { const parsed = parseStaticDHCPLine(line); @@ -403,31 +403,26 @@ function renderStaticDHCPTable() { .attr("title", "Add new line after this") .attr("data-toggle", "tooltip"); - const tr = $("") + const tr = $(""); if (parsed === "advanced") { - tr.addClass("table-warning") - .append( - 'Advanced settings present in line ' + - (idx + 1) + - "", - ) + tr.addClass("table-warning").append( + 'Advanced settings present in line ' + + (idx + 1) + + "" + ); // Keep the original data tr.data("original-line", line); // Disable the save button on advanced rows - saveBtn - .addClass("disabled") - .prop("disabled", true) - .attr("title", "Disabled"); - + saveBtn.addClass("disabled").prop("disabled", true).attr("title", "Disabled"); } else { // Append 3 cells containing parsed values, with placeholder for empty hwaddr tr.append($('').text(parsed.hwaddr)) .append($('').text(parsed.ipaddr)) .append( - $('').text(parsed.hostname), + $('').text(parsed.hostname) ); } @@ -529,7 +524,8 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function $(document).on("input blur paste", "#StaticDHCPTable td.static-hostname", function () { const val = $(this).text().trim(); // Hostnames must not contain spaces, commas, or characters invalid in DNS names - const hostnameValidator = /^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$/v; + const hostnameValidator = + /^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$/v; if (val && !hostnameValidator.test(val)) { $(this).addClass("table-danger"); $(this).removeClass("table-success"); From 82033d0936e963bdc628f66d360abaaabb9012c5 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Wed, 1 Apr 2026 22:34:18 -0300 Subject: [PATCH 19/35] Group both "on click" functions for save-static-row button Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 86c3c53851..07fc711611 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -314,6 +314,12 @@ $(document).on("click", ".save-static-row:not(.disabled)", function () { $("#dhcp-hosts").val(lines.join("\n")); // Optionally, re-render the table to reflect changes renderStaticDHCPTable(); + + // On save, re-enable all buttons (except buttons with class "disabled") and remove the hint + $( + "#StaticDHCPTable .save-static-row:not(.disabled), #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" + ).prop("disabled", false); + $(".edit-hint-row").remove(); }); // Delete button for each row removes that line from the textarea and updates the table @@ -366,14 +372,6 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function ( } }); -// On save, re-enable all buttons (except buttons with class "disabled") and remove the hint -$(document).on("click", ".save-static-row", function () { - $( - "#StaticDHCPTable .save-static-row:not(.disabled), #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" - ).prop("disabled", false); - $(".edit-hint-row").remove(); -}); - // On table redraw, ensure all buttons are enabled and hints are removed function renderStaticDHCPTable() { const tbody = $("#StaticDHCPTable tbody"); From 04d76f865d2061df9122a3be96b2978e264178e2 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Sat, 11 Apr 2026 23:39:50 -0300 Subject: [PATCH 20/35] Adjust DHCP "edit-hint" and action buttons - only show save button when needed - use fixed size and right aligned text for button column - highlight cells only on error and remove the highlight when is fixed - replace a few inline CSS styles with proper HTML tags - adjust CSS on specific themes Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 93 +++++++++++++++++++------------------ style/pi-hole.css | 6 +++ style/themes/lcars.css | 1 + 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 07fc711611..b4342d327e 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -286,10 +286,10 @@ function parseStaticDHCPLine(line) { }; } -// Save button for each row updates only that line in the textarea, if it doesn't contain the class ".disabled" -$(document).on("click", ".save-static-row:not(.disabled)", function () { +// Save button for each row updates only that line in the textarea +$(document).on("click", ".save-static-row", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); - const row = $(this).closest("tr"); + const row = $(`tr[data-row="${rowIdx}"]`); const hwaddr = row.find(".static-hwaddr").text().trim(); const ipaddr = row.find(".static-ipaddr").text().trim(); const hostname = row.find(".static-hostname").text().trim(); @@ -312,14 +312,19 @@ $(document).on("click", ".save-static-row:not(.disabled)", function () { lines[rowIdx] = hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; $("#dhcp-hosts").val(lines.join("\n")); - // Optionally, re-render the table to reflect changes - renderStaticDHCPTable(); - // On save, re-enable all buttons (except buttons with class "disabled") and remove the hint - $( - "#StaticDHCPTable .save-static-row:not(.disabled), #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" - ).prop("disabled", false); - $(".edit-hint-row").remove(); + // On save, re-enable all buttons, except from all rows currently being edited, ... + $("#StaticDHCPTable tbody tr:not(:has(+ tr.edit-hint-row), .edit-hint-row) button").prop( + "disabled", + false, + ); + // ... then enable the buttons on the current row, ... + row.find(".delete-static-row, .add-static-row").prop("disabled", false); + // ... remove highlight colors from all cells on this row ... + $("td", row).removeClass("table-danger"); + // ... and remove the save button and the hint + $(this).remove(); + row.next(".edit-hint-row").remove(); }); // Delete button for each row removes that line from the textarea and updates the table @@ -358,17 +363,31 @@ $(() => { $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function () { const row = $(this).closest("tr"); // Disable all action buttons in all rows - $( - "#StaticDHCPTable .save-static-row, #StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row" - ).prop("disabled", true); - // Enable only the save button in the current row - row.find(".save-static-row").prop("disabled", false); - // Show a hint below the current row if not already present + $("#StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row").prop("disabled", true); + + // Add save button and show a hint below the current row if not already present if (!row.next().hasClass("edit-hint-row")) { + const idx = row.attr("data-row"); + const saveBtn = $( + '' + ) + .attr("data-row", idx) + .attr("title", "Confirm changes to this line") + .attr("data-toggle", "tooltip"); + + // Add the save button to the actions column + row.find("td").last().prepend(saveBtn, " "); + + // Create a table row containing the edit hint and the save button + const hintRow = $('') + .append( + $( + 'Please confirm changes using the green button, then click "Save & Apply" before leaving the page.' + ) + ); + row.next(".edit-hint-row").remove(); // Remove any existing hint - row.after( - 'Please confirm changes using the green button, then click "Save & Apply" before leaving the page.' - ); + row.after(hintRow); // Add the created hint } }); @@ -380,13 +399,6 @@ function renderStaticDHCPTable() { for (const [idx, line] of lines.entries()) { const parsed = parseStaticDHCPLine(line); - const saveBtn = $( - '' - ) - .attr("data-row", idx) - .attr("title", "Confirm changes to this line") - .attr("data-toggle", "tooltip"); - const delBtn = $( '' ) @@ -401,36 +413,30 @@ function renderStaticDHCPTable() { .attr("title", "Add new line after this") .attr("data-toggle", "tooltip"); - const tr = $(""); + const tr = $("").attr("data-row", idx); if (parsed === "advanced") { tr.addClass("table-warning").append( - 'Advanced settings present in line ' + - (idx + 1) + - "" + `Advanced settings present in line ${idx + 1}` ); // Keep the original data tr.data("original-line", line); - - // Disable the save button on advanced rows - saveBtn.addClass("disabled").prop("disabled", true).attr("title", "Disabled"); } else { + const cell = ''; // Append 3 cells containing parsed values, with placeholder for empty hwaddr - tr.append($('').text(parsed.hwaddr)) - .append($('').text(parsed.ipaddr)) - .append( - $('').text(parsed.hostname) - ); + tr.append($(cell).addClass("static-hwaddr").text(parsed.hwaddr)) + .append($(cell).addClass("static-ipaddr").text(parsed.ipaddr)) + .append($(cell).addClass("static-hostname").text(parsed.hostname)); } // Append a last cell containing the buttons - tr.append($("").append(saveBtn, " ", delBtn, " ", addBtn)); + tr.append($('').append(delBtn, " ", addBtn)); tbody.append(tr); } - tbody.find(".save-static-row, .delete-static-row, .add-static-row").prop("disabled", false); + tbody.find(".delete-static-row, .add-static-row").prop("disabled", false); tbody.find(".edit-hint-row").remove(); } @@ -497,10 +503,8 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-hwaddr", function const val = $(this).text().trim(); if (val && !utils.validateMAC(val)) { $(this).addClass("table-danger"); - $(this).removeClass("table-success"); $(this).attr("title", "Invalid MAC address format"); } else { - $(this).addClass("table-success"); $(this).removeClass("table-danger"); $(this).attr("title", ""); } @@ -510,10 +514,8 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function const val = $(this).text().trim(); if (val && !(utils.validateIPv4(val) || utils.validateIPv6(val))) { $(this).addClass("table-danger"); - $(this).removeClass("table-success"); $(this).attr("title", "Invalid IP address format"); } else { - $(this).addClass("table-success"); $(this).removeClass("table-danger"); $(this).attr("title", ""); } @@ -526,10 +528,9 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-hostname", functi /^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$/v; if (val && !hostnameValidator.test(val)) { $(this).addClass("table-danger"); - $(this).removeClass("table-success"); $(this).attr("title", "Invalid hostname: only letters, digits, hyphens, and dots allowed"); } else { - $(this).removeClass("table-danger table-success"); + $(this).removeClass("table-danger"); $(this).attr("title", ""); } }); diff --git a/style/pi-hole.css b/style/pi-hole.css index f6ecf472d1..2184252a53 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1635,6 +1635,12 @@ div.dhcp-hosts-wrapper { transition: opacity 200ms ease-in-out; } +#StaticDHCPTable th:last-of-type, +#StaticDHCPTable td:last-of-type { + width: 106px; + text-align: right; +} + #StaticDHCPTable td.table-danger { background-color: #700000; color: #fff; diff --git a/style/themes/lcars.css b/style/themes/lcars.css index c6fad3c06a..6ba1d831de 100644 --- a/style/themes/lcars.css +++ b/style/themes/lcars.css @@ -203,6 +203,7 @@ th { .btn-xs { padding: 0 5px 2px; border-radius: 5px; + min-width: auto; } .btn.btn-box-tool { From 81cec5dde51b6d57190236e459dd7a0b66a9e328 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Fri, 3 Apr 2026 05:07:24 -0300 Subject: [PATCH 21/35] Use a different approach to update .dhcp-hosts height - replace DOMContentLoaded listener with a function and call it when needed - use javascript to update the CSS variable `--num-lines` - use CSS to calculate the elements height Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 10 +++++----- style/pi-hole.css | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index b4342d327e..4fd34bed59 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -438,6 +438,7 @@ function renderStaticDHCPTable() { tbody.find(".delete-static-row, .add-static-row").prop("disabled", false); tbody.find(".edit-hint-row").remove(); + showLineNumbers(); } // Copy button for each lease row copies the lease as a new static lease line @@ -452,7 +453,7 @@ $(document).on("click", ".copy-to-static", function () { }); // Add line numbers to the textarea for static DHCP hosts -document.addEventListener("DOMContentLoaded", function () { +function showLineNumbers() { const textarea = document.getElementById("dhcp-hosts"); const linesElem = document.getElementById("dhcp-hosts-lines"); let lastLineCount = 0; @@ -473,13 +474,12 @@ document.addEventListener("DOMContentLoaded", function () { "letterSpacing", "lineHeight", "padding", - "height", ]) { linesElem.style[property] = globalThis.getComputedStyle(textarea)[property]; } - // Match height and scroll - linesElem.style.height = textarea.offsetHeight > 0 ? textarea.offsetHeight + "px" : "auto"; + // Update "--num-lines" variable and let CSS handle the height + $(".dhcp-hosts-wrapper").css("--num-lines", lines); } function syncScroll() { @@ -497,7 +497,7 @@ document.addEventListener("DOMContentLoaded", function () { updateLineNumbers(true); syncScroll(); } -}); +} $(document).on("input blur paste", "#StaticDHCPTable td.static-hwaddr", function () { const val = $(this).text().trim(); diff --git a/style/pi-hole.css b/style/pi-hole.css index 2184252a53..3b24a8d02a 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1651,6 +1651,30 @@ div.dhcp-hosts-wrapper { color: #fff; } +/* +Calculate the total height using the number of lines and line-height. Also account for borders and padding. +Use the same values used by ".form-control" class: +line-height=1.42857; font-size=14px; +border-top=1px + padding-top=6px + padding-bottom=6px + border-bottom=1px + +(num-lines * line-height * font-size) + 14px +*/ +.dhcp-hosts-wrapper { + --num-lines: 1; + --max-lines: 12; + --total-height: calc((min(var(--num-lines), var(--max-lines)) * 1.42857 * 14px) + 14px); +} + +#dhcp-hosts { + height: var(--total-height); +} + +#dhcp-hosts-lines { + height: var(--total-height); + border: 1px solid transparent; + border-right-color: #ddd; +} + .save-static-row.disabled { opacity: 0.15; } From 08050a1f54219a8edcd7008f34757da70d92acce Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Sun, 12 Apr 2026 00:09:31 -0300 Subject: [PATCH 22/35] Rearrange CSS used for #StaticDHCPTable and #dhcp-hosts - move and group some CSS selectors in the file, to make editing easier - use semi-transparent background color for .table-danger - reduce specificity for StaticDHCPTable cells and .line-numbers - remove unused and duplicated selectors Signed-off-by: RD WebDesign --- style/pi-hole.css | 76 ++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 47 deletions(-) diff --git a/style/pi-hole.css b/style/pi-hole.css index 3b24a8d02a..a9ff81fa07 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1576,41 +1576,6 @@ table.dataTable tbody > tr > .selected { margin: 0 4px; } -textarea.field-sizing-content { - min-block-size: 3.5rlh; - max-block-size: 20rlh; - min-inline-size: 20ch; - field-sizing: content; -} - -textarea.no-wrap { - white-space: pre; - overflow-x: auto; - overflow-y: auto; - resize: none; -} - -div.line-numbers { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - text-align: right; - color: #aaa; - border-right: 1px solid #ddd; - padding: 8px 4px 8px 0; - font-family: monospace; - font-size: 13px; - min-width: 2em; - overflow-x: hidden; - overflow-y: hidden; -} - -div.dhcp-hosts-wrapper { - position: relative; - display: flex; - gap: 0.5em; -} - /* Used in interfaces page */ .list-group-item { background: transparent; @@ -1641,14 +1606,22 @@ div.dhcp-hosts-wrapper { text-align: right; } -#StaticDHCPTable td.table-danger { - background-color: #700000; - color: #fff; +#StaticDHCPTable .table-danger { + background-color: #d337; } -#StaticDHCPTable td.table-success { - background-color: #599900; - color: #fff; +textarea.field-sizing-content { + min-block-size: 3.5rlh; + max-block-size: 20rlh; + min-inline-size: 20ch; + field-sizing: content; +} + +textarea.no-wrap { + white-space: pre; + overflow-x: auto; + overflow-y: auto; + resize: none; } /* @@ -1663,20 +1636,29 @@ border-top=1px + padding-top=6px + padding-bottom=6px + border-bottom=1px --num-lines: 1; --max-lines: 12; --total-height: calc((min(var(--num-lines), var(--max-lines)) * 1.42857 * 14px) + 14px); + position: relative; + display: flex; + gap: 0; } #dhcp-hosts { height: var(--total-height); } -#dhcp-hosts-lines { - height: var(--total-height); +.line-numbers { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + text-align: right; + color: #aaa; border: 1px solid transparent; border-right-color: #ddd; -} - -.save-static-row.disabled { - opacity: 0.15; + padding: 8px 4px 8px 0; + font-size: 14px; + min-width: 2em; + overflow: hidden; + height: var(--total-height); + flex: 0 0 auto; } /* Used in query log page */ From 9ec6c529fc0604dc19cc5805d22ecc8ae9e18981 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Sat, 4 Apr 2026 22:35:29 -0300 Subject: [PATCH 23/35] Disable the textarea during table editing to prevent loss of unsaved data If enabled, the textarea can be edited, which triggers a table rewrite, resulting in the loss of all unsaved changes made to the table rows. This commit keeps the textarea disabled until all edited rows are saved. Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 4fd34bed59..292221ab24 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -313,18 +313,20 @@ $(document).on("click", ".save-static-row", function () { hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; $("#dhcp-hosts").val(lines.join("\n")); - // On save, re-enable all buttons, except from all rows currently being edited, ... - $("#StaticDHCPTable tbody tr:not(:has(+ tr.edit-hint-row), .edit-hint-row) button").prop( - "disabled", - false, - ); - // ... then enable the buttons on the current row, ... - row.find(".delete-static-row, .add-static-row").prop("disabled", false); - // ... remove highlight colors from all cells on this row ... - $("td", row).removeClass("table-danger"); - // ... and remove the save button and the hint + // On save remove the save button and the hint $(this).remove(); row.next(".edit-hint-row").remove(); + // and remove highlight colors from all cells on this row + $("td", row).removeClass("table-danger"); + + // Check if all rows were already saved (no rows are still being edited) + if ($("#StaticDHCPTable .edit-hint-row").length == 0) { + // Re-enable all table buttons + $("#StaticDHCPTable button").prop("disabled", false); + // and re-enable the textarea + $("#dhcp-hosts").prop("disabled", false); + $("#dhcp-hosts").prop("title", ""); + } }); // Delete button for each row removes that line from the textarea and updates the table @@ -386,8 +388,15 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function ( ) ); - row.next(".edit-hint-row").remove(); // Remove any existing hint - row.after(hintRow); // Add the created hint + // Disable the textarea to avoid losing unsaved changes to the table + $("#dhcp-hosts").prop("disabled", true); + $("#dhcp-hosts").prop("title", "Disabled.\nConfirm all changes above to enable this field"); + + // Remove any previously existing hint + row.next(".edit-hint-row").remove(); + + // Add the created hint + row.after(hintRow); } }); From b985503e6fb38da9e3ffbea691526adec8170df1 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Sun, 5 Apr 2026 01:43:07 -0300 Subject: [PATCH 24/35] Use CSS to show confirmation message when editing static DHCP table rows Remove the javascript code used to add and remove "hints" and replace it with a single CSS pseudo-element after the table, instead of one message per line. Also, adjust LCARS message position. Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 26 ++++---------------------- style/pi-hole.css | 36 ++++++++++++++++++++++++++++++++++++ style/themes/lcars.css | 17 +++++++++++++++++ 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 292221ab24..3f4447ce66 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -313,19 +313,17 @@ $(document).on("click", ".save-static-row", function () { hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; $("#dhcp-hosts").val(lines.join("\n")); - // On save remove the save button and the hint + // On save remove the save button $(this).remove(); - row.next(".edit-hint-row").remove(); // and remove highlight colors from all cells on this row $("td", row).removeClass("table-danger"); // Check if all rows were already saved (no rows are still being edited) - if ($("#StaticDHCPTable .edit-hint-row").length == 0) { + if ($("#StaticDHCPTable .save-static-row").length === 0) { // Re-enable all table buttons $("#StaticDHCPTable button").prop("disabled", false); // and re-enable the textarea $("#dhcp-hosts").prop("disabled", false); - $("#dhcp-hosts").prop("title", ""); } }); @@ -367,8 +365,8 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function ( // Disable all action buttons in all rows $("#StaticDHCPTable .delete-static-row, #StaticDHCPTable .add-static-row").prop("disabled", true); - // Add save button and show a hint below the current row if not already present - if (!row.next().hasClass("edit-hint-row")) { + // Add save button (a hint asking to click on the button will be shown below the table - CSS pseudo-element) + if (row.find(".save-static-row").length === 0) { const idx = row.attr("data-row"); const saveBtn = $( '' @@ -380,23 +378,8 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function ( // Add the save button to the actions column row.find("td").last().prepend(saveBtn, " "); - // Create a table row containing the edit hint and the save button - const hintRow = $('') - .append( - $( - 'Please confirm changes using the green button, then click "Save & Apply" before leaving the page.' - ) - ); - // Disable the textarea to avoid losing unsaved changes to the table $("#dhcp-hosts").prop("disabled", true); - $("#dhcp-hosts").prop("title", "Disabled.\nConfirm all changes above to enable this field"); - - // Remove any previously existing hint - row.next(".edit-hint-row").remove(); - - // Add the created hint - row.after(hintRow); } }); @@ -446,7 +429,6 @@ function renderStaticDHCPTable() { } tbody.find(".delete-static-row, .add-static-row").prop("disabled", false); - tbody.find(".edit-hint-row").remove(); showLineNumbers(); } diff --git a/style/pi-hole.css b/style/pi-hole.css index a9ff81fa07..8115d8fb30 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1624,6 +1624,24 @@ textarea.no-wrap { resize: none; } +/* Show hint message on the static DHCP lease table, when at least one Save button exists */ +div:has(> .table-responsive #StaticDHCPTable) { + position: relative; +} +div:has(> .table-responsive #StaticDHCPTable .save-static-row)::before { + content: 'Please confirm changes using the green button, then click "Save & Apply" before leaving the page.'; + position:absolute; + display: block; + padding: 4px 12px; + max-width: calc(100% - 30px); + right: 15px; + bottom: 0px; + color: #000; + font-style: italic; + text-align: right; + background: #eedd88; +} + /* Calculate the total height using the number of lines and line-height. Also account for borders and padding. Use the same values used by ".form-control" class: @@ -1641,6 +1659,24 @@ border-top=1px + padding-top=6px + padding-bottom=6px + border-bottom=1px gap: 0; } +/* Message displayed when the textarea is disabled because there are table rows being edited */ +.dhcp-hosts-wrapper:has(textarea[disabled]):hover::before { + content: "Please confirm all changes above before editing this field."; + display: block; + position: absolute; + width: 380px; + padding: 8px; + top: 50%; + left: 50%; + transform: translate(-45%, -50%); + text-align: center; + font-style: italic; + color: #000; + background: #ed8; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); + cursor: not-allowed; +} + #dhcp-hosts { height: var(--total-height); } diff --git a/style/themes/lcars.css b/style/themes/lcars.css index 6ba1d831de..8668d64d59 100644 --- a/style/themes/lcars.css +++ b/style/themes/lcars.css @@ -2185,3 +2185,20 @@ td.highlight { .help-block { color: #76808a; } + +div:has(> .table-responsive #StaticDHCPTable .save-static-row)::after { + padding: 1px 10px 3px; + background: #fd8; + border-radius: 5px; +} +.dhcp-hosts-wrapper:has(textarea[disabled]):hover::before { + padding: 6px 8px 8px; + background: #fd8; + border-radius: 5px; + width: 400px; + transform: translate(-47%, -50%); + box-shadow: 0 0 10px 2px #000; +} +.line-numbers { + border-color: transparent; +} From 9cdf3830d003bb81a8c49b2159e48ccaaf31b5cc Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Sun, 5 Apr 2026 23:50:26 -0300 Subject: [PATCH 25/35] Fix tooltips on dynamically created buttons (Static DHCP lease table) Dinamically created buttons were not showing bootstrap tooltips because the event listener was created too early, before the buttons were created (only previuosly existing buttons would show a tooltip). Now, tooptip() function is called once, on the body element. This attaches an event listener to the body. Every dynamically created button will display the tooltip, without the need to call tooltip() function. We need to manually call .tooltip("hide"), to remove tooltips when buttons are clicked. Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index 3f4447ce66..f59d4f360c 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -13,9 +13,7 @@ let dhcpLeaesTable = null; const toasts = {}; // DHCP leases tooltips -$(() => { - $('[data-toggle="tooltip"]').tooltip({ html: true, container: "body" }); -}); +$("body").tooltip({ selector: '[data-toggle="tooltip"]', container: "body" }); function renderHostnameCLID(data, type) { // Display and search content @@ -313,9 +311,9 @@ $(document).on("click", ".save-static-row", function () { hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; $("#dhcp-hosts").val(lines.join("\n")); - // On save remove the save button - $(this).remove(); - // and remove highlight colors from all cells on this row + // On save, hide the tooltip and remove the save button + $(this).tooltip("hide").remove(); + // then remove highlight colors from all cells on this row $("td", row).removeClass("table-danger"); // Check if all rows were already saved (no rows are still being edited) @@ -333,6 +331,8 @@ $(document).on("click", ".delete-static-row", function () { const lines = $("#dhcp-hosts").val().split(/\r?\n/v); lines.splice(rowIdx, 1); $("#dhcp-hosts").val(lines.join("\n")); + // Hide the tooltip + $(this).tooltip("hide"); renderStaticDHCPTable(); }); @@ -342,6 +342,8 @@ $(document).on("click", ".add-static-row", function () { const lines = $("#dhcp-hosts").val().split(/\r?\n/v); lines.splice(rowIdx + 1, 0, ""); $("#dhcp-hosts").val(lines.join("\n")); + // Hide the tooltip + $(this).tooltip("hide"); renderStaticDHCPTable(); // Focus the new row after render setTimeout(() => { From bc0111316336848bda1b7d09b78195d283ad7910 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Tue, 7 Apr 2026 16:54:08 -0300 Subject: [PATCH 26/35] Enforce hostname and IPv6 validation on the static DHCP table Only accept valid hostnames: Previously, invalid hostnames were trigerring the cell highlighting, but invalid entries were still accepted when using the green "save" button. Now, hostnames are saved only if they pass the validation, like other values. Only allow IPv6 enclosed in square brackets: The previous validation function used to validate IPv6 didn't accept brackets, resulting in errors when an IPv6 was typed directly on the table cell. A new function was created to validate IPv6 enclosed in brackets. Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 22 ++++++++-------------- scripts/js/utils.js | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index f59d4f360c..f6ffae050a 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -261,13 +261,9 @@ function parseStaticDHCPLine(line) { const hwaddr = haveMAC ? parts[0].trim() : ""; // Check if the first or second part is a valid IPv4 or IPv6 address - const hasSquareBrackets0 = parts[0][0] === "[" && parts[0].at(-1) === "]"; - const ipv60 = hasSquareBrackets0 ? parts[0].slice(1, -1) : parts[0]; - const hasSquareBrackets1 = parts.length > 1 && parts[1][0] === "[" && parts[1].at(-1) === "]"; - const ipv61 = hasSquareBrackets1 ? parts[1].slice(1, -1) : parts.length > 1 ? parts[1] : ""; - const firstIsValidIP = utils.validateIPv4(parts[0]) || utils.validateIPv6(ipv60); + const firstIsValidIP = utils.validateIPv4(parts[0]) || utils.validateIPv6Brackets(parts[0]); const secondIsValidIP = - parts.length > 1 && (utils.validateIPv4(parts[1]) || utils.validateIPv6(ipv61)); + parts.length > 1 && (utils.validateIPv4(parts[1]) || utils.validateIPv6Brackets(parts[1])); const ipaddr = firstIsValidIP ? parts[0].trim() : secondIsValidIP ? parts[1].trim() : ""; const haveIP = ipaddr.length > 0; @@ -294,12 +290,13 @@ $(document).on("click", ".save-static-row", function () { // Validate MAC and IP before saving const macValid = !hwaddr || utils.validateMAC(hwaddr); - const ipValid = !ipaddr || utils.validateIPv4(ipaddr) || utils.validateIPv6(ipaddr); - if (!macValid || !ipValid) { + const ipValid = !ipaddr || utils.validateIPv4(ipaddr) || utils.validateIPv6Brackets(ipaddr); + const nameValid = !hostname || utils.validateHostnameStrict(hostname); + if (!macValid || !ipValid || !nameValid) { utils.showAlert( "error", "fa-times", - "Cannot save: Invalid MAC or IP address", + "Cannot save: Invalid value found on the table", "Please correct the highlighted fields before saving." ); return; @@ -505,7 +502,7 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-hwaddr", function $(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function () { const val = $(this).text().trim(); - if (val && !(utils.validateIPv4(val) || utils.validateIPv6(val))) { + if (val && !(utils.validateIPv4(val) || utils.validateIPv6Brackets(val))) { $(this).addClass("table-danger"); $(this).attr("title", "Invalid IP address format"); } else { @@ -516,10 +513,7 @@ $(document).on("input blur paste", "#StaticDHCPTable td.static-ipaddr", function $(document).on("input blur paste", "#StaticDHCPTable td.static-hostname", function () { const val = $(this).text().trim(); - // Hostnames must not contain spaces, commas, or characters invalid in DNS names - const hostnameValidator = - /^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$/v; - if (val && !hostnameValidator.test(val)) { + if (val && !utils.validateHostnameStrict(val)) { $(this).addClass("table-danger"); $(this).attr("title", "Invalid hostname: only letters, digits, hyphens, and dots allowed"); } else { diff --git a/scripts/js/utils.js b/scripts/js/utils.js index c3c49aca7d..e2a00bc7ab 100644 --- a/scripts/js/utils.js +++ b/scripts/js/utils.js @@ -263,6 +263,18 @@ function validateIPv6(ip) { return validateIPv6CIDR(ipv6WithCIDR); } +function validateIPv6Brackets(ip) { + // Check if the IPv6 is enclosed in brackets and return in case of failure + if (!ip.trim().startsWith("[") || !ip.trim().endsWith("]")) { + return false; + } + + // Strip brackets before validating the IPv6 + const ipWithoutBrackets = ip.replaceAll("[", "").replaceAll("]", ""); + // Validate the ip + return validateIPv6(ipWithoutBrackets); +} + function validateMAC(mac) { // Format: xx:xx:xx:xx:xx:xx where each xx is 0-9 or a-f (case insensitive) // Also allows dashes as separator, e.g. xx-xx-xx-xx-xx-xx @@ -275,6 +287,13 @@ function validateHostname(name) { return namevalidator.test(name.trim()); } +function validateHostnameStrict(name) { + // Hostnames must not contain spaces, commas, or characters invalid in DNS names + const hostnameValidator = + /^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$/v; + return hostnameValidator.test(name.trim()); +} + // set bootstrap-select defaults function setBsSelectDefaults() { const bsSelectDefaults = $.fn.selectpicker.Constructor.DEFAULTS; @@ -728,11 +747,13 @@ globalThis.utils = (function () { validateIPv4, validateIPv6CIDR, validateIPv6, + validateIPv6Brackets, setBsSelectDefaults, stateSaveCallback, stateLoadCallback, validateMAC, validateHostname, + validateHostnameStrict, addFromQueryLog, addTD, toPercent, From 39538c11258d648c1862b668cdcf9a7018be7cbb Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Wed, 22 Apr 2026 17:57:37 -0300 Subject: [PATCH 27/35] Avoid overlaping text when showing confirmation messages on small screens Also fix LCARS theme styles for the new interface Signed-off-by: RD WebDesign --- settings-dhcp.lp | 2 +- style/pi-hole.css | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/settings-dhcp.lp b/settings-dhcp.lp index 6a1deba156..419c71a65a 100644 --- a/settings-dhcp.lp +++ b/settings-dhcp.lp @@ -173,7 +173,7 @@ mg.include('scripts/lua/settings_header.lp','r')
-
+
diff --git a/style/pi-hole.css b/style/pi-hole.css index 8115d8fb30..e9876f968e 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1600,6 +1600,11 @@ table.dataTable tbody > tr > .selected { transition: opacity 200ms ease-in-out; } +#StaticDHCPTable, +.table-responsive:has(#StaticDHCPTable) { + margin: 0; +} + #StaticDHCPTable th:last-of-type, #StaticDHCPTable td:last-of-type { width: 106px; @@ -1610,6 +1615,26 @@ table.dataTable tbody > tr > .selected { background-color: #d337; } +/* Show hint message on the static DHCP lease table, when at least one Save button exists */ +div:has(> .table-responsive #StaticDHCPTable) { + position: relative; + margin-bottom: 15px; +} +div:has(> .table-responsive #StaticDHCPTable .save-static-row)::after { + content: 'Please confirm changes using the green button, then click "Save & Apply" before leaving the page.'; + position: relative; + margin: 2px 0 15px; + padding: 4px 12px; + width: -moz-fit-content; + width: fit-content; + max-width: 100%; + color: #000; + font-style: italic; + text-align: right; + background: #eedd88; + float: right; +} + textarea.field-sizing-content { min-block-size: 3.5rlh; max-block-size: 20rlh; @@ -1624,24 +1649,6 @@ textarea.no-wrap { resize: none; } -/* Show hint message on the static DHCP lease table, when at least one Save button exists */ -div:has(> .table-responsive #StaticDHCPTable) { - position: relative; -} -div:has(> .table-responsive #StaticDHCPTable .save-static-row)::before { - content: 'Please confirm changes using the green button, then click "Save & Apply" before leaving the page.'; - position:absolute; - display: block; - padding: 4px 12px; - max-width: calc(100% - 30px); - right: 15px; - bottom: 0px; - color: #000; - font-style: italic; - text-align: right; - background: #eedd88; -} - /* Calculate the total height using the number of lines and line-height. Also account for borders and padding. Use the same values used by ".form-control" class: @@ -1675,6 +1682,7 @@ border-top=1px + padding-top=6px + padding-bottom=6px + border-bottom=1px background: #ed8; box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); cursor: not-allowed; + z-index: 10; } #dhcp-hosts { From f06cc5291ccf04239cdb99c162310354b2006c6a Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Fri, 24 Apr 2026 01:24:13 -0300 Subject: [PATCH 28/35] Add cancel button, to allow restore edited unsaved table rows When editing a row, two new buttons will be visible: Confirm and Cancel Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 58 ++++++++++++++++++++++++++++++++----- style/pi-hole.css | 10 ++++++- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index f6ffae050a..410a61d142 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -308,10 +308,14 @@ $(document).on("click", ".save-static-row", function () { hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; $("#dhcp-hosts").val(lines.join("\n")); - // On save, hide the tooltip and remove the save button + // Update "data-original-line" to containing the new saved values + row.attr("data-original-line", lines[rowIdx]); + + // Hide the tooltips and remove Save and Cancel buttons + $(this).siblings(".cancel-static-row").tooltip("hide").remove(); $(this).tooltip("hide").remove(); // then remove highlight colors from all cells on this row - $("td", row).removeClass("table-danger"); + $("td", row).blur(); // Check if all rows were already saved (no rows are still being edited) if ($("#StaticDHCPTable .save-static-row").length === 0) { @@ -322,6 +326,42 @@ $(document).on("click", ".save-static-row", function () { } }); +// Cancel button: restores the original line value when editing a row +$(document).on("click", ".cancel-static-row", function () { + const rowIdx = Number.parseInt($(this).data("row"), 10); + const row = $(`tr[data-row="${rowIdx}"]`); + + // Get the original values + const originalLine = row.attr("data-original-line"); + + if (originalLine) { + const values = originalLine.split(","); + + // Reset with original values, ensuring index exists + row.find(".static-hwaddr").text(values[0] ? values[0].trim() : ""); + row.find(".static-ipaddr").text(values[1] ? values[1].trim() : ""); + row.find(".static-hostname").text(values[2] ? values[2].trim() : ""); + } else { + // Optional: Handle empty state, e.g., clear fields or set defaults + row.find(".static-hwaddr, .static-ipaddr, .static-hostname").text(""); + } + + // Trigger "blur" event to remove highlight colors and titles from all cells on this row + row.find(".static-hwaddr, .static-ipaddr, .static-hostname").blur(); + + // Then hide the tooltip and remove Save and Cancel buttons + $(this).siblings(".save-static-row").tooltip("hide").remove(); + $(this).tooltip("hide").remove(); + + // Check if all rows were already saved or canceled (no rows are still being edited) + if ($("#StaticDHCPTable .save-static-row").length === 0) { + // Re-enable all table buttons + $("#StaticDHCPTable button").prop("disabled", false); + // and re-enable the textarea + $("#dhcp-hosts").prop("disabled", false); + } +}); + // Delete button for each row removes that line from the textarea and updates the table $(document).on("click", ".delete-static-row", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); @@ -373,9 +413,15 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function ( .attr("data-row", idx) .attr("title", "Confirm changes to this line") .attr("data-toggle", "tooltip"); + const cancelBtn = $( + '' + ) + .attr("data-row", idx) + .attr("title", "Cancel changes and restore original values") + .attr("data-toggle", "tooltip"); // Add the save button to the actions column - row.find("td").last().prepend(saveBtn, " "); + row.find("td").last().prepend(saveBtn, " ", cancelBtn, " "); // Disable the textarea to avoid losing unsaved changes to the table $("#dhcp-hosts").prop("disabled", true); @@ -404,15 +450,13 @@ function renderStaticDHCPTable() { .attr("title", "Add new line after this") .attr("data-toggle", "tooltip"); - const tr = $("").attr("data-row", idx); + // Create the new row - store the original data, in case we need to restore the values + const tr = $("").attr("data-row", idx).attr("data-original-line", line); if (parsed === "advanced") { tr.addClass("table-warning").append( `` ); - - // Keep the original data - tr.data("original-line", line); } else { const cell = ''; // Append 3 cells containing parsed values, with placeholder for empty hwaddr diff --git a/style/pi-hole.css b/style/pi-hole.css index e9876f968e..4bd5650057 100644 --- a/style/pi-hole.css +++ b/style/pi-hole.css @@ -1607,7 +1607,7 @@ table.dataTable tbody > tr > .selected { #StaticDHCPTable th:last-of-type, #StaticDHCPTable td:last-of-type { - width: 106px; + width: 122px; text-align: right; } @@ -1615,6 +1615,14 @@ table.dataTable tbody > tr > .selected { background-color: #d337; } +#StaticDHCPTable .btn-xs { + width: 24px; + height: 24px; +} +.btn-xs .fa-fw { + width: 1em; +} + /* Show hint message on the static DHCP lease table, when at least one Save button exists */ div:has(> .table-responsive #StaticDHCPTable) { position: relative; From 8c6bf7b8134822379743fc43d812a84faef2f3e0 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Fri, 24 Apr 2026 19:59:52 -0300 Subject: [PATCH 29/35] Adjust the layout and add new styles to better accommodate the help text The intention here is to improve readability Signed-off-by: RD WebDesign --- settings-dhcp.lp | 37 ++++++++++++++++++++++++------------- style/pi-hole.css | 14 ++++++++++++++ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/settings-dhcp.lp b/settings-dhcp.lp index 419c71a65a..bdd6841b78 100644 --- a/settings-dhcp.lp +++ b/settings-dhcp.lp @@ -190,36 +190,47 @@ mg.include('scripts/lua/settings_header.lp','r') +
+
+ +

Specify per host parameters for the DHCP server. This allows a machine with a particular hardware address to be always allocated the same hostname, IP address and lease time. A hostname specified like this overrides any supplied by the DHCP client on the machine. It is also allowable to omit the hardware address and include the hostname, in which case the IP address and lease times will apply to any machine claiming that name.

+

Each entry should be on a separate line, and should be of the form:

+
[<hwaddr>][,id:<client_id>|*][,set:<tag>][,tag:<tag>][,<ipaddr>][,<hostname>][,<lease_time>][,ignore]
+
+
+
-

Specify per host parameters for the DHCP server. This allows a machine with a particular hardware address to be always allocated the same hostname, IP address and lease time. A hostname specified like this overrides any supplied by the DHCP client on the machine. It is also allowable to omit the hardware address and include the hostname, in which case the IP address and lease times will apply to any machine claiming that name.

+ +
    +
  • Only one entry per MAC address is allowed.
  • +
  • IPv6 addresses must be enclosed in square brackets like [2001:db8::1].
  • +
  • Only letters, digits, hyphens and dots are allowed in hostnames.
  • +
+
-

Each entry should be on a separate line, and should be of the form:

-
[<hwaddr>][,id:<client_id>|*][,set:<tag>][,tag:<tag>][,<ipaddr>][,<hostname>][,<lease_time>][,ignore]
-

Only one entry per MAC address is allowed. IPv6 addresses must be enclosed in square brackets like [2001:db8::1].

-

Examples:

-
    -
  • 00:20:e0:3b:13:af,192.168.0.123
    tells Pi-hole to give the machine with hardware address 00:20:e0:3b:13:af the address 192.168.0.123
     
  • -
  • 00:20:e0:3b:13:af,laptop
    tells Pi-hole to give the machine with hardware address 00:20:e0:3b:13:af the name laptop
     
  • -
  • 00:20:e0:3b:13:af,192.168.0.123,laptop,infinite
    tells Pi-hole to give the machine with hardware address 00:20:e0:3b:13:af the address 192.168.0.123, the name laptop, and an infinite DHCP lease
     
  • -
+ +
00:20:e0:3b:13:af,192.168.0.123
tells Pi-hole to give the machine with hardware address 00:20:e0:3b:13:af the address 192.168.0.123
+
00:20:e0:3b:13:af,laptop
tells Pi-hole to give the machine with hardware address 00:20:e0:3b:13:af the name laptop
+
00:20:e0:3b:13:af,192.168.0.123,laptop,infinite
tells Pi-hole to give the machine with hardware address 00:20:e0:3b:13:af the address 192.168.0.123, the name laptop, and an infinite DHCP lease
+
00:20:e0:3b:13:af,[2001:db8::1]
tells Pi-hole to give the machine with hardware address 00:20:e0:3b:13:af the address [2001:db8::1]
-
+
-

Advanced description

+

Advanced settings help

-
'; - // Append 3 cells containing parsed values, with placeholder for empty hwaddr - tr.append($(cell).addClass("static-hwaddr").text(parsed.hwaddr)) - .append($(cell).addClass("static-ipaddr").text(parsed.ipaddr)) - .append($(cell).addClass("static-hostname").text(parsed.hostname)); + + // Populate the 3 cells. Validate and highlight in case of failure + const tdHwaddr = $(cell).addClass("static-hwaddr").text(parsed.hwaddr); + if (parsed.hwaddr && !utils.validateMAC(parsed.hwaddr)) { + tdHwaddr.addClass("table-danger"); + } + + const tdIpaddr = $(cell).addClass("static-ipaddr").text(parsed.ipaddr); + if ( + parsed.ipaddr && + !(utils.validateIPv4(parsed.ipaddr) || utils.validateIPv6Brackets(parsed.ipaddr)) + ) { + tdIpaddr.addClass("table-danger"); + } + + const tdHostname = $(cell).addClass("static-hostname").text(parsed.hostname); + if (parsed.hostname && !utils.validateHostnameStrict(parsed.hostname)) { + tdHostname.addClass("table-danger"); + } + + // Append 3 cells containing parsed values + tr.append(tdHwaddr, tdIpaddr, tdHostname); } // Append a last cell containing the buttons From 5eef250a4d2ff21e5c7fa4ab363920a276403d88 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Tue, 19 May 2026 17:55:12 -0300 Subject: [PATCH 35/35] Use regex flag "u" to fix errors after merging development branch Signed-off-by: RD WebDesign --- scripts/js/settings-dhcp.js | 14 +++++++------- scripts/js/utils.js | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/js/settings-dhcp.js b/scripts/js/settings-dhcp.js index a92188a467..551c8cc11c 100644 --- a/scripts/js/settings-dhcp.js +++ b/scripts/js/settings-dhcp.js @@ -248,7 +248,7 @@ function parseStaticDHCPLine(line) { }; // Advanced if line contains id:, set:, tag, or "*": - if (/id:|set:|tag:\*/v.test(line)) return "advanced"; + if (/id:|set:|tag:\*/u.test(line)) return "advanced"; // Split the line by commas and trim whitespace const parts = line.split(",").map(s => s.trim()); @@ -262,7 +262,7 @@ function parseStaticDHCPLine(line) { for (const part of parts) { // Advanced if the part is "infinite", "ignore" or a lease time value - if (/^(infinite|ignore|\d+(w|W|d|D|h|H|m|M|s|S))$/v.test(part)) return "advanced"; + if (/^(infinite|ignore|\d+(w|W|d|D|h|H|m|M|s|S))$/u.test(part)) return "advanced"; if (part.includes(":")) { if (part.startsWith("[") && part.endsWith("]")) { @@ -278,7 +278,7 @@ function parseStaticDHCPLine(line) { // Potentially a MAC Address (we allow invalid values here. The table will highlight them) hwaddr = part; } - } else if (/^[.0-9]+$/v.test(part)) { + } else if (/^[.0-9]+$/u.test(part)) { // Advanced if more than one IP was found if (ipaddr) return "advanced"; @@ -322,7 +322,7 @@ $(document).on("click", ".save-static-row", function () { return; } - const lines = $("#dhcp-hosts").val().split(/\r?\n/v); + const lines = $("#dhcp-hosts").val().split(/\r?\n/u); // Only update if at least one field is non-empty lines[rowIdx] = hwaddr || ipaddr || hostname ? [hwaddr, ipaddr, hostname].filter(Boolean).join(",") : ""; @@ -385,7 +385,7 @@ $(document).on("click", ".cancel-static-row", function () { // Delete button for each row removes that line from the textarea and updates the table $(document).on("click", ".delete-static-row", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); - const lines = $("#dhcp-hosts").val().split(/\r?\n/v); + const lines = $("#dhcp-hosts").val().split(/\r?\n/u); lines.splice(rowIdx, 1); $("#dhcp-hosts").val(lines.join("\n")); // Hide the tooltip @@ -396,7 +396,7 @@ $(document).on("click", ".delete-static-row", function () { // Add button for each row inserts a new empty line after this row $(document).on("click", ".add-static-row", function () { const rowIdx = Number.parseInt($(this).data("row"), 10); - const lines = $("#dhcp-hosts").val().split(/\r?\n/v); + const lines = $("#dhcp-hosts").val().split(/\r?\n/u); lines.splice(rowIdx + 1, 0, ""); $("#dhcp-hosts").val(lines.join("\n")); // Hide the tooltip @@ -452,7 +452,7 @@ $(document).on("focus input", "#StaticDHCPTable td[contenteditable]", function ( function renderStaticDHCPTable() { const tbody = $("#StaticDHCPTable tbody"); tbody.empty(); - const lines = $("#dhcp-hosts").val().split(/\r?\n/v); + const lines = $("#dhcp-hosts").val().split(/\r?\n/u); for (const [idx, line] of lines.entries()) { const parsed = parseStaticDHCPLine(line); diff --git a/scripts/js/utils.js b/scripts/js/utils.js index bf52d1df92..893c658e03 100644 --- a/scripts/js/utils.js +++ b/scripts/js/utils.js @@ -277,7 +277,7 @@ function validateIPv6Brackets(ip) { function validateMAC(mac) { // Format: xx:xx:xx:xx:xx:xx where each xx is 0-9 or a-f (case insensitive) // Also allows dashes as separator, e.g. xx-xx-xx-xx-xx-xx - const macvalidator = /^(?:[\da-f]{2}([:\-]))(?:[\da-f]{2}\1){4}[\da-f]{2}$/iu; + const macvalidator = /^(?:[\da-f]{2}([:-]))(?:[\da-f]{2}\1){4}[\da-f]{2}$/iu; return macvalidator.test(mac.trim()); } @@ -289,7 +289,7 @@ function validateHostname(name) { function validateHostnameStrict(name) { // Hostnames must not contain spaces, commas, or characters invalid in DNS names const hostnameValidator = - /^[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)*$/v; + /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/u; return hostnameValidator.test(name.trim()); }
Advanced settings present in line ${idx + 1}