From: Giorgio Ravera Date: Sun, 11 Jan 2026 00:57:38 +0000 (+0100) Subject: several improvements in hots (sort, validate, layout) X-Git-Tag: v0.0.1~33 X-Git-Url: http://git.giorgioravera.it/?a=commitdiff_plain;h=c5a41e29e0cd07b090edc6e623c020a687443496;p=network-manager.git several improvements in hots (sort, validate, layout) --- diff --git a/frontend/css/hosts.css b/frontend/css/hosts.css index 4740467..9435d17 100644 --- a/frontend/css/hosts.css +++ b/frontend/css/hosts.css @@ -30,3 +30,17 @@ td.actions { border-radius: 6px; font-size: 14px; } + +/* ================================ + Sort arrows (specific to hosts) + ================================ */ +.sort-arrow { + display: inline-block; + width: 1em; + text-align: center; + margin-left: .25rem; + color: var(--text-dark); +} + +.sort-arrow.asc::after { content: "▲"; } +.sort-arrow.desc::after { content: "▼"; } diff --git a/frontend/hosts.html b/frontend/hosts.html index 41ee140..c8350a6 100644 --- a/frontend/hosts.html +++ b/frontend/hosts.html @@ -52,12 +52,12 @@ - - - - - - + + + + + + diff --git a/frontend/js/hosts.js b/frontend/js/hosts.js index 9c2e47e..4c14165 100644 --- a/frontend/js/hosts.js +++ b/frontend/js/hosts.js @@ -1,16 +1,47 @@ let editingHostId = null; let sortDirection = {}; +let lastSort = null; // { colIndex: number, ascending: boolean } +const stringCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); // ----------------------------- -// Validate the IP address format +// Validate the IPv4 address format // ----------------------------- -function isValidIP(ip) { +function isValidIPv4(ip) { if (!ip || !ip.trim()) return true; // empty is allowed - const ipv4 = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/; - const ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::1)$/; + return ipv4.test(ip); +} + +// ----------------------------- +// Validate the IPv6 address format +// ----------------------------- +function isValidIPv6(ip) { + if (!ip || !ip.trim()) return true; // empty is allowed + // Parser robusto (gestisce '::') + let s = ip.toLowerCase().trim(); + const dbl = s.indexOf("::"); + let parts = []; + if (dbl >= 0) { + const left = s.slice(0, dbl).split(":").filter(Boolean); + const right = s.slice(dbl + 2).split(":").filter(Boolean); + const missing = 8 - (left.length + right.length); + if (missing < 0) return false; + parts = [...left, ...Array(missing).fill("0"), ...right]; + } else { + parts = s.split(":"); + if (parts.length !== 8) return false; + } + return parts.every(p => /^[0-9a-f]{1,4}$/.test(p)); +} - return ipv4.test(ip) || ipv6.test(ip); +// ----------------------------- +// Validate the MAC address format +// ----------------------------- +function isValidMAC(mac) { + const s = (mac ?? "").trim().toLowerCase().replace(/[\s:\-\.]/g, ""); + // vuoto consentito + if (s === "") return true; + return /^[0-9a-f]{12}$/.test(s); } // ----------------------------- @@ -18,6 +49,10 @@ function isValidIP(ip) { // ----------------------------- async function loadHosts() { const res = await fetch("/api/hosts"); + if (!res.ok) { + showToast("Errore nel caricamento degli host", false); + return; + } const hosts = await res.json(); const tbody = document.querySelector("#hosts-table tbody"); @@ -25,32 +60,83 @@ async function loadHosts() { hosts.forEach(h => { const tr = document.createElement("tr"); - tr.innerHTML = ` - - - - - - - + + // Name + const tdName = document.createElement("td"); + const nameVal = (h.name ?? "").toString(); + tdName.textContent = nameVal; + if (nameVal) tdName.setAttribute("data-value", nameVal.toLowerCase()); + tr.appendChild(tdName); + + // IPv4 + const tdIPv4 = document.createElement("td"); + const ipv4Raw = (h.ipv4 ?? "").trim(); + tdIPv4.textContent = ipv4Raw; + if (ipv4Raw) tdIPv4.setAttribute("data-value", ipv4Raw); + tr.appendChild(tdIPv4); + + // IPv6 + const tdIPv6 = document.createElement("td"); + const ipv6Raw = (h.ipv6 ?? "").trim(); + tdIPv6.textContent = ipv6Raw; + if (ipv6Raw) tdIPv6.setAttribute("data-value", ipv6Raw.toLowerCase()); + tr.appendChild(tdIPv6); + + // MAC + const tdMAC = document.createElement("td"); + const macRaw = (h.mac ?? "").trim(); + tdMAC.textContent = macRaw; + const macNorm = macRaw.toLowerCase().replace(/[\s:\-\.]/g, ""); + if (macNorm) tdMAC.setAttribute("data-value", macNorm); + tr.appendChild(tdMAC); + + // Note + const tdNote = document.createElement("td"); + const noteVal = (h.note ?? "").toString(); + tdNote.textContent = noteVal; + if (noteVal) tdNote.setAttribute("data-value", noteVal.toLowerCase()); + tr.appendChild(tdNote); + + // SSL (icon) + const tdSSL = document.createElement("td"); + const sslEnabled = !!h.ssl_enabled; // 1/true -> true + if (sslEnabled) { + tdSSL.innerHTML = "✔"; + tdSSL.setAttribute("data-value", "true"); + tdSSL.setAttribute("aria-label", "SSL attivo"); + } else { + tdSSL.setAttribute("data-value", "false"); + tdSSL.setAttribute("aria-label", "SSL non attivo"); + } + tr.appendChild(tdSSL); + + // Actions + const tdActions = document.createElement("td"); + tdActions.className = "actions"; + tdActions.innerHTML = ` + + + + + + `; + tr.appendChild(tdActions); + tbody.appendChild(tr); }); + + if (lastSort) { + sortDirection[lastSort.colIndex] = !lastSort.ascending; + sortTable(lastSort.colIndex); + } } // ----------------------------- @@ -58,6 +144,11 @@ async function loadHosts() { // ----------------------------- async function editHost(id) { const res = await fetch(`/api/hosts/${id}`); + if (!res.ok) { + console.error(`Errore nel recupero host ${id}:`, res.status); + showToast("Errore nel recupero host", false); + return; + } const host = await res.json(); // Store the ID of the host being edited @@ -69,7 +160,7 @@ async function editHost(id) { document.getElementById("hostIPv6").value = host.ipv6 || ""; document.getElementById("hostMAC").value = host.mac || ""; document.getElementById("hostNote").value = host.note || ""; - document.getElementById("hostSSL").checked = host.ssl_enabled === 1; + document.getElementById("hostSSL").checked = !!host.ssl_enabled; document.getElementById("addHostModal").style.display = "flex"; } @@ -108,15 +199,21 @@ async function saveHost() { showToast("Name is required", false); return; // stop here, do NOT send the request } - // Validate IP format - if (!isValidIP(document.getElementById("hostIPv4").value)) { + // Validate IPv4 format + if (!isValidIPv4(document.getElementById("hostIPv4").value)) { showToast("Invalid IPv4 format", false); return; } - if (!isValidIP(document.getElementById("hostIPv6").value)) { + // Validate IPv6 format + if (!isValidIPv6(document.getElementById("hostIPv6").value)) { showToast("Invalid IPv6 format", false); return; } + // Validate MAC format + if (!isValidMAC(document.getElementById("hostMAC").value)) { + showToast("Invalid MAC format", false); + return; + } const payload = { name: document.getElementById("hostName").value, @@ -130,26 +227,26 @@ async function saveHost() { try { if (editingHostId !== null) { // UPDATE EXISTING HOST - await fetch(`/api/hosts/${editingHostId}`, { + const res = await fetch(`/api/hosts/${editingHostId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); - + if (!res.ok) throw new Error(`Update failed: ${res.status}`); showToast("Host updated successfully"); } else { // CREATE NEW HOST - await fetch("/api/hosts", { + const res = await fetch("/api/hosts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); - + if (!res.ok) throw new Error(`Create failed: ${res.status}`); showToast("Host added successfully"); } closeAddHostModal(); - loadHosts(); + await loadHosts(); } catch (err) { console.error(err); @@ -163,11 +260,9 @@ async function saveHost() { async function deleteHost(id) { try { const res = await fetch(`/api/hosts/${id}`, { method: "DELETE" }); - if (!res.ok) { throw new Error("Delete failed"); } - showToast("Host removed successfully"); } catch (err) { @@ -175,7 +270,7 @@ async function deleteHost(id) { showToast("Error while removing host", false); } - loadHosts(); + await loadHosts(); } // ----------------------------- @@ -210,11 +305,175 @@ function filterHosts() { // ----------------------------- // Clear search on ESC key // ----------------------------- -function clearSearch() { +async function clearSearch() { const input = document.getElementById("searchInput"); input.value = ""; input.blur(); - loadHosts(); + await loadHosts(); +} + +/** + * Parser per tipi di dato: + * - number: riconosce float, anche con virgole migliaia + * - date: tenta Date.parse su formati comuni + * - ipv4: ordina per valore numerico dei 4 ottetti + * - ipv6: espande '::' e ordina come BigInt 128-bit + * - mac: normalizza e ordina come BigInt 48-bit + * - version: ordina semver-like "1.2.10" + * - string: predefinito (locale-aware, case-insensitive, con numerico) + */ +function parseByType(value, type) { + const v = (value ?? "").trim(); + + if (v === "") return { type: "empty", value: null }; + + switch (type) { + case "number": { + // Rimuove separatori di migliaia (spazio, apostrofo, punto prima di gruppi da 3) + // e converte la virgola decimale in punto. + const norm = v.replace(/[\s'’\.](?=\d{3}\b)/g, "").replace(",", "."); + const n = Number(norm); + return isNaN(n) ? { type: "string", value: v.toLowerCase() } : { type: "number", value: n }; + } + + case "date": { + let time = Date.parse(v); + if (isNaN(time)) { + // prova formato DD/MM/YYYY [HH:mm] + const m = v.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/); + if (m) { + const [_, d, mo, y, hh = "0", mm = "0"] = m; + time = Date.parse(`${y}-${mo.padStart(2,"0")}-${d.padStart(2,"0")}T${hh.padStart(2,"0")}:${mm.padStart(2,"0")}:00`); + } + } + return isNaN(time) ? { type: "string", value: v.toLowerCase() } : { type: "date", value: time }; + } + + case "ipv4": { + const m = v.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); + if (m) { + const a = +m[1], b = +m[2], c = +m[3], d = +m[4]; + if ([a,b,c,d].every(n => Number.isInteger(n) && n >= 0 && n <= 255)) { + const num = ((a << 24) | (b << 16) | (c << 8) | d) >>> 0; // 32-bit unsigned + return { type: "ipv4", value: num }; + } + } + return { type: "string", value: v.toLowerCase() }; + } + + case "ipv6": { + // Espansione '::' e parsing 8 gruppi hex da 16 bit ? BigInt 128-bit + let s = v.toLowerCase().trim(); + const dbl = s.indexOf("::"); + let parts = []; + if (dbl >= 0) { + const left = s.slice(0, dbl).split(":").filter(Boolean); + const right = s.slice(dbl + 2).split(":").filter(Boolean); + const missing = 8 - (left.length + right.length); + if (missing < 0) return { type: "string", value: v.toLowerCase() }; + parts = [...left, ...Array(missing).fill("0"), ...right]; + } else { + parts = s.split(":"); + if (parts.length !== 8) return { type: "string", value: v.toLowerCase() }; + } + if (!parts.every(p => /^[0-9a-f]{1,4}$/.test(p))) { + return { type: "string", value: v.toLowerCase() }; + } + let big = 0n; + for (const p of parts) { + big = (big << 16n) + BigInt(parseInt(p, 16)); + } + return { type: "ipv6", value: big }; + } + + case "mac": { + // Accetta :, -, ., spazi o formato compatto; ordina come BigInt 48-bit + let s = v.toLowerCase().trim().replace(/[\s:\-\.]/g, ""); + if (!/^[0-9a-f]{12}$/.test(s)) { + return { type: "string", value: v.toLowerCase() }; + } + const mac = BigInt("0x" + s); + return { type: "mac", value: mac }; + } + + case "version": { + const segs = v.split(".").map(s => (/^\d+$/.test(s) ? Number(s) : s.toLowerCase())); + return { type: "version", value: segs }; + } + + default: + case "string": + return { type: "string", value: v.toLowerCase() }; + } +} + +/** + * Comparatore generico in base al tipo + */ +function comparator(aParsed, bParsed) { + // Celle vuote in fondo in asc + if (aParsed.type === "empty" && bParsed.type === "empty") return 0; + if (aParsed.type === "empty") return 1; + if (bParsed.type === "empty") return -1; + + // Se i tipi differiscono (capita se fallback a string), imponi una gerarchia: + // tipi “numerici” prima delle stringhe + if (aParsed.type !== bParsed.type) { + const rank = { number: 0, date: 0, ipv4: 0, ipv6: 0, mac: 0, version: 0, string: 1 }; + return (rank[aParsed.type] ?? 1) - (rank[bParsed.type] ?? 1); + } + + switch (aParsed.type) { + case "number": + case "date": + case "ipv4": + return aParsed.value - bParsed.value; + + case "ipv6": + case "mac": { + // Confronto BigInt + if (aParsed.value === bParsed.value) return 0; + return aParsed.value < bParsed.value ? -1 : 1; + } + + case "version": { + const a = aParsed.value, b = bParsed.value; + const len = Math.max(a.length, b.length); + for (let i = 0; i < len; i++) { + const av = a[i] ?? 0; + const bv = b[i] ?? 0; + if (typeof av === "number" && typeof bv === "number") { + if (av !== bv) return av - bv; + } else { + const as = String(av), bs = String(bv); + const cmp = stringCollator.compare(as, bs); + if (cmp !== 0) return cmp; + } + } + return 0; + } + + case "string": + default: + return stringCollator.compare(aParsed.value, bParsed.value); + } +} + +/** + * Aggiorna UI delle frecce e ARIA in thead + */ +function updateSortUI(table, colIndex, ascending) { + const ths = table.querySelectorAll("thead th"); + ths.forEach((th, i) => { + const arrow = th.querySelector(".sort-arrow"); + th.setAttribute("aria-sort", i === colIndex ? (ascending ? "ascending" : "descending") : "none"); + th.classList.toggle("is-sorted", i === colIndex); + if (arrow) { + arrow.classList.remove("asc", "desc"); + if (i === colIndex) arrow.classList.add(ascending ? "asc" : "desc"); + } + // Se usi pulsanti
Name IPv4 IPv6 MAC Note SSL Name IPv4 IPv6 MAC Note SSL Actions
${h.name}${h.ipv4 || ""}${h.ipv6 || ""}${h.mac || ""}${h.note || ""}${h.ssl_enabled ? "✔" : ""} - - - - - - - - - - - -