]> git.giorgioravera.it Git - network-manager.git/commitdiff
Created js file for services and commons, added creation of aliases file for dns
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Wed, 18 Feb 2026 23:16:37 +0000 (00:16 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Wed, 18 Feb 2026 23:16:37 +0000 (00:16 +0100)
12 files changed:
backend/main.py
backend/routes/aliases.py
backend/routes/dns.py
backend/routes/hosts.py
frontend/aliases.html
frontend/favicon.ico [new file with mode: 0644]
frontend/hosts.html
frontend/js/aliases.js
frontend/js/common.js [new file with mode: 0644]
frontend/js/hosts.js
frontend/js/login.js
frontend/js/services.js [new file with mode: 0644]

index 1e9ba84ed878277e07edd6a0e0a26e4f089b2c7d..846847ef3a7807a06a8adef29e89f5fb262f829f 100644 (file)
@@ -282,6 +282,21 @@ def css_variables(request: Request):
 def css_layout(request: Request):
     return FileResponse(os.path.join(settings.FRONTEND_DIR, "css/layout.css"))
 
+# JS Common
+@app.get("/js/common.js")
+def js_common(request: Request):
+    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/common.js"))
+
+# JS Services
+@app.get("/js/services.js")
+def js_services(request: Request):
+    return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/services.js"))
+
+# favicon
+@app.get("/favicon.ico")
+def favicon(request: Request):
+    return FileResponse(os.path.join(settings.FRONTEND_DIR, "favicon.ico"))
+
 # ------------------------------------------------------------------------------
 # Entry-point
 # ------------------------------------------------------------------------------
index 41b3ee83070794a1a4d57b8c6d10da27681d3df5..be7c2c5beb69de8badc8cfac8b5cfd02ae72f113 100644 (file)
@@ -32,7 +32,7 @@ def aliases(request: Request):
 
 # Serve aliases.js
 @router.get("/js/aliases.js")
-def css_aliases():
+def js_aliases():
     return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/aliases.js"))
 
 # ---------------------------------------------------------
index 43c9b9c5d119a9cdc22fe963ab6245df735192bf..6cd51f1df8998f607f20364dbe1c2b174a47115f 100644 (file)
@@ -10,6 +10,7 @@ import ipaddress
 import time
 # Import local modules
 from backend.db.hosts import get_hosts
+from backend.db.aliases import get_aliases
 # Import Settings
 from settings.settings import settings
 # Import Logging
@@ -49,6 +50,16 @@ async def api_dns_reload(request: Request):
                     line = f"{rev}\t\t IN PTR\t{h.get('name')}.{settings.DOMAIN}\n"
                     f.write(line)
 
+        # Get Aliases List
+        hosts = get_aliases()
+
+        # Save DNS Aliases Configuration
+        path = settings.DNS_ALIAS_FILE
+        with open(path, "w", encoding="utf-8") as f:
+            for h in hosts:
+                line = f"{h.get('name')}\t\t IN\tCNAME\t{h.get('target')}\n"
+                f.write(line)
+
         # RELOAD DNS
 
         took_ms = (time.monotonic_ns() - start_ns) / 1_000_000
index 0aa6f3635ff48ff215b6ebdd62f7978d7ca28913..15c03680bce858bee9a6dc5eb6527eb142ed798d 100644 (file)
@@ -32,7 +32,7 @@ def hosts(request: Request):
 
 # Serve hosts.js
 @router.get("/js/hosts.js")
-def css_hosts():
+def js_hosts():
     return FileResponse(os.path.join(settings.FRONTEND_DIR, "js/hosts.js"))
 
 # ---------------------------------------------------------
index f7beff881ac863926e2ba66668a68325bdf71033..c3ac1a1b68494588412e7f68d0f3a8df42ee0562 100644 (file)
@@ -66,7 +66,6 @@
                             type="text"
                             id="searchInput"
                             placeholder="Ricerca..."
-                            oninput="filterAliases()"
                             class="form-control form-control-sm"
                             aria-label="Search alias"
                         >
     </section>
 
     <!-- Tabella -->
-    <table id="hosts-table" class="table table-bordered table-hover align-middle">
+    <table id="dataTable" class="table table-bordered table-hover align-middle">
         <thead class="table-light">
             <tr>
-                <th data-type="string" onclick="sortTable(0)">Alias  <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" onclick="sortTable(1)">Target <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" onclick="sortTable(2)">Note   <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" onclick="sortTable(3)">SSL    <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string">Actions <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true">Alias    <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true">Target   <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true">Note     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true">SSL      <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="false">Actions <span class="sort-arrow" aria-hidden="true"></span></th>
             </tr>
         </thead>
         <tbody></tbody>
                 </div>
 
                 <div class="modal-body">
-                    <form id="addAliasForm" onsubmit="return handleAddAliasSubmit(event)">
+                    <form id="addAliasForm">
                         <div class="mb-2">
                             <label for="aliasName" class="form-label">Alias</label>
                             <input type="text" id="aliasName" class="form-control" required>
     </div>
 
     <!-- Scripts -->
-    <script src="js/aliases.js"></script>
-    <script src="js/session.js"></script>
+    <script type="module" src="js/aliases.js"></script>
+    <script type="module" src="js/session.js"></script>
 
     <!-- Bootstrap JS -->
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
diff --git a/frontend/favicon.ico b/frontend/favicon.ico
new file mode 100644 (file)
index 0000000..76b1f31
Binary files /dev/null and b/frontend/favicon.ico differ
index a00f4975da473d7538c55cafabeb18570c3b7ec4..9874bf229e0845b2a445bf54db8b541a05c51e52 100644 (file)
@@ -66,7 +66,6 @@
                             type="text"
                             id="searchInput"
                             placeholder="Ricerca..."
-                            oninput="filterHosts()"
                             class="form-control form-control-sm"
                             aria-label="Search hosts"
                         >
     </section>
 
     <!-- Tabella -->
-    <table id="hosts-table" class="table table-bordered table-hover align-middle">
+    <table id="dataTable" class="table table-bordered table-hover align-middle">
         <thead class="table-light">
             <tr>
-                <th data-type="string" onclick="sortTable(0)">Hostname <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="ipv4"   onclick="sortTable(1)">IPv4     <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="ipv6"   onclick="sortTable(2)">IPv6     <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="mac"    onclick="sortTable(3)">MAC      <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" onclick="sortTable(4)">Note     <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string" onclick="sortTable(5)">SSL      <span class="sort-arrow" aria-hidden="true"></span></th>
-                <th data-type="string">Actions <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true">Hostname <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="ipv4"   data-sortable="true">IPv4     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="ipv6"   data-sortable="true">IPv6     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="mac"    data-sortable="true">MAC      <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true">Note     <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="true">SSL      <span class="sort-arrow" aria-hidden="true"></span></th>
+                <th data-type="string" data-sortable="false">Actions <span class="sort-arrow" aria-hidden="true"></span></th>
             </tr>
         </thead>
         <tbody></tbody>
                 </div>
 
                 <div class="modal-body">
-                    <form id="addHostForm" onsubmit="return handleAddHostSubmit(event)">
+                    <form id="addHostForm">
                         <div class="mb-2">
                             <label for="hostName" class="form-label">Hostname</label>
                             <input type="text" id="hostName" class="form-control" required>
     </div>
 
     <!-- Scripts -->
-    <script src="js/hosts.js"></script>
-    <script src="js/session.js"></script>
+    <script type="module" src="js/hosts.js"></script>
+    <script type="module" src="js/session.js"></script>
 
     <!-- Bootstrap JS -->
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
index a6e3a5170f6a9299dd517ef082d07d02b97c6a83..96017566ade7a72194e4ed4e31fd9d7d4a21194e 100644 (file)
@@ -1,15 +1,12 @@
-// -----------------------------
-// Configuration parameters
-// -----------------------------
-let timeoutToast = 3000; // milliseconds
+// Import common js
+import { showToast, sortTable, initSortableTable, resetSorting } from './common.js';
+import { reloadDNS, reloadDHCP } from './services.js';
 
 // -----------------------------
 // State variables
 // -----------------------------
 let editingAliasId = null;
-let sortDirection = {};
-let lastSort = null; // { colIndex: number, ascending: boolean }
-const stringCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
+const sortState = { sortDirection: {}, lastSort: null };
 
 // -----------------------------
 // Load all aliases into the table
@@ -56,9 +53,9 @@ async function loadAliases() {
     }
 
     // DOM Reference
-    const tbody = document.querySelector("#hosts-table tbody");
+    const tbody = document.querySelector("#dataTable tbody");
     if (!tbody) {
-        console.warn('Element "#hosts-table tbody" not found in DOM.');
+        console.warn('Element "#dataTable tbody" not found in DOM.');
         return;
     }
 
@@ -184,7 +181,7 @@ async function loadAliases() {
     if (typeof lastSort === "object" && lastSort && Array.isArray(sortDirection)) {
         if (Number.isInteger(lastSort.colIndex)) {
             sortDirection[lastSort.colIndex] = !lastSort.ascending;
-            sortTable(lastSort.colIndex);
+            sortTable(lastSort.colIndex, sortState);
         }
     }
 }
@@ -454,136 +451,12 @@ async function handleDeleteAlias(e, el) {
     return false;
 }
 
-// -----------------------------
-// Handle reload DNS action
-// -----------------------------
-async function handleReloadDNS() {
-    try {
-        // Fetch data
-        const res = await fetch(`/api/dns/reload`, {
-            method: 'POST',
-            headers: { 'Accept': 'application/json' },
-        });
-
-        // Success without JSON
-        if (res.status === 204) {
-            showToast('DNS reload successfully', true);
-            return true;
-        }
-
-        // Check content-type to avoid parsing errors
-        const contentType = res.headers.get("content-type") || "";
-        if (!contentType.includes("application/json")) {
-            const err = new Error(`${res.status}: ${res.statusText}`);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Check JSON
-        let data;
-        try {
-            data = await res.json();
-        } catch {
-            throw new Error('Invalid JSON payload');
-        }
-
-        // Check JSON errors
-        if (!res.ok) {
-            const serverMsg = data?.detail?.message?.trim();
-            const base = `Error reloading DNS`;
-            const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Success
-        showToast(data?.message || 'DNS reload successfully', true);
-        return true;
-
-    } catch (err) {
-        console.error(err?.message || "Error reloading DNS");
-        showToast(err?.message || "Error reloading DNS", false);
-    }
-
-    return false;
-}
-
-// -----------------------------
-// Handle reload DHCP action
-// -----------------------------
-async function handleReloadDHCP() {
-    try {
-        // Fetch data
-        const res = await fetch(`/api/dhcp/reload`, {
-            method: 'POST',
-            headers: { 'Accept': 'application/json' },
-        });
-
-        // Success without JSON
-        if (res.status === 204) {
-            showToast('DHCP reload successfully', true);
-            return true;
-        }
-
-        // Check content-type to avoid parsing errors
-        const contentType = res.headers.get("content-type") || "";
-        if (!contentType.includes("application/json")) {
-            const err = new Error(`${res.status}: ${res.statusText}`);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Check JSON
-        let data;
-        try {
-            data = await res.json();
-        } catch {
-            throw new Error('Error reloading DHCP: Invalid JSON payload');
-        }
-
-        // Check JSON errors
-        if (!res.ok) {
-            const serverMsg = data?.detail?.message?.trim();
-            const base = `Error reloadin DHCP`;
-            const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Success
-        showToast(data?.message || 'DHCP reload successfully', true);
-        return true;
-
-    } catch (err) {
-        console.error(err?.message || "Error reloading DHCP");
-        showToast(err?.message || "Error reloading DHCP", false);
-    }
-
-    return false;
-}
-
-// -----------------------------
-// Display a temporary notification message
-// -----------------------------
-function showToast(message, success = true) {
-    const toast = document.getElementById("toast");
-    toast.textContent = message;
-
-    toast.style.background = success ? "#28a745" : "#d9534f"; // green / red
-
-    toast.classList.add("show");
-
-    setTimeout(() => {
-        toast.classList.remove("show");
-    }, timeoutToast);
-}
-
 // -----------------------------
 // filter aliases in the table
 // -----------------------------
 function filterAliases() {
     const query = document.getElementById("searchInput").value.toLowerCase();
-    const rows = document.querySelectorAll("#hosts-table tbody tr");
+    const rows = document.querySelectorAll("#dataTable tbody tr");
 
     rows.forEach(row => {
         const text = row.textContent.toLowerCase();
@@ -601,258 +474,6 @@ async function clearSearch() {
     await loadAliases();
 }
 
-/**
- * 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 <button>, puoi aggiornare aria-pressed/aria-label qui.
-    });
-}
-
-// -----------------------------
-// Sort the table by column
-// -----------------------------
-function sortTable(colIndex) {
-    const table = document.getElementById("hosts-table");
-    if (!table) return;
-
-    const tbody = table.querySelector("tbody");
-    if (!tbody) return;
-
-    const rows = Array.from(tbody.querySelectorAll("tr"));
-    const ths = table.querySelectorAll("thead th");
-    const type = ths[colIndex]?.dataset?.type || "string";
-
-    // Toggle direction
-    sortDirection[colIndex] = !sortDirection[colIndex];
-    const ascending = !!sortDirection[colIndex];
-    const direction = ascending ? 1 : -1;
-
-    // Pre-parsing per performance
-    const parsed = rows.map((row, idx) => {
-        const cell = row.children[colIndex];
-        const raw = (cell?.getAttribute("data-value") ?? cell?.innerText ?? "").trim();
-        return { row, idx, parsed: parseByType(raw, type) };
-    });
-
-    parsed.sort((a, b) => {
-        const c = comparator(a.parsed, b.parsed);
-        return c !== 0 ? c * direction : (a.idx - b.idx); // tie-breaker
-    });
-
-    // Re-append in un DocumentFragment (più efficiente)
-    const frag = document.createDocumentFragment();
-    parsed.forEach(p => frag.appendChild(p.row));
-    tbody.appendChild(frag);
-
-    updateSortUI(table, colIndex, ascending);
-
-    lastSort = { colIndex, ascending };
-}
-
-/**
- * Opzionale: inizializza aria-sort='none'
- */
-function initSortableTable() {
-    const table = document.getElementById("hosts-table");
-    if (!table) return;
-    const ths = table.querySelectorAll("thead th");
-    ths.forEach(th => th.setAttribute("aria-sort", "none"));
-}
-
-// -----------------------------
-// Reset sorting arrows and directions
-// -----------------------------
-function resetSorting() {
-    // azzera lo stato
-    sortDirection = {};
-
-    const table = document.getElementById("hosts-table");
-    if (!table) return;
-
-    // reset ARIA e classi
-    table.querySelectorAll("thead th").forEach(th => {
-        th.setAttribute("aria-sort", "none");
-        th.classList.remove("is-sorted");
-        const arrow = th.querySelector(".sort-arrow");
-        if (arrow) arrow.classList.remove("asc", "desc");
-    });
-
-    lastSort = null;
-}
-
-// -----------------------------
-// RELOAD DNS
-// -----------------------------
-function reloadDNS() {
-    // Implement DNS reload logic here
-    showToast("DNS reloaded successfully");
-}
-
-// -----------------------------
-// RELOAD DHCP
-// -----------------------------
-function reloadDHCP() {
-    // Implement DHCP reload logic here
-    showToast("DHCP reloaded successfully");
-}
-
 // -----------------------------
 // Action Handlers
 // -----------------------------
@@ -865,28 +486,29 @@ const actionHandlers = {
   },
 
   // Reload DNS
-  async reloadDns()  { handleReloadDNS();  },
+  async reloadDns(e, el) { reloadDNS(); },
 
   // Reload DHCP
-  async reloadDhcp() { handleReloadDHCP(); },
+  async reloadDhcp(e, el) { reloadDHCP(); },
 };
 
 // -----------------------------
 // DOMContentLoaded: initialize everything
 // -----------------------------
 document.addEventListener("DOMContentLoaded", async () => {
-    // 1) Init UI sort (aria-sort, arrows)
+
+    // Init UI sort (aria-sort, arrows)
     initSortableTable();
 
-    // 2) Load data (aliases)
+    // Load data (aliases)
     try {
         await loadAliases();
     } catch (err) {
-        console.error("Error loading aliases:", err);
-        showToast("Error loading aliases:", false);
+        console.error(err?.message || "Error loading aliases");
+        showToast(err?.message || "Error loading aliases:", false);
     }
 
-    // 3) search bar
+    // search bar
     const input = document.getElementById("searchInput");
     if (input) {
         // clean input on load
@@ -898,13 +520,14 @@ document.addEventListener("DOMContentLoaded", async () => {
             if (e.key === "Escape") {
                 e.preventDefault();     // evita side-effect (es. chiusure di modali del browser)
                 e.stopPropagation();    // evita che arrivi al listener globale
-                resetSorting();
+                resetSorting(sortState);
                 clearSearch();          // svuota input e ricarica tabella (come definito nella tua funzione)
+                filterHosts('');        // ripristina tabella
             }
         });
     }
 
-    // 4) global ESC key listener to clear search and reset sorting
+    // global ESC key listener to clear search and reset sorting
     document.addEventListener("keydown", (e) => {
         // Ignore if focus is in a typing field
         const tag = (e.target.tagName || "").toLowerCase();
@@ -914,12 +537,13 @@ document.addEventListener("DOMContentLoaded", async () => {
         if (e.key === "Escape" && !isTypingField) {
             // Prevent default form submission
             e.preventDefault();
-            resetSorting();
+            resetSorting(sortState);
             clearSearch();
+            filterHosts('');
         }
     });
 
-    // 5) Modal show/hidden events to prepare/reset the form
+    // Modal show/hidden events to prepare/reset the form
     const modalEl = document.getElementById('addAliasModal');
     if (modalEl) {
 
@@ -928,7 +552,7 @@ document.addEventListener("DOMContentLoaded", async () => {
 
         // When shown, determine Add or Edit mode
         modalEl.addEventListener('show.bs.modal', async (ev) => {
-            const lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit)
+            lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit)
             const formEl = document.getElementById('addAliasForm');
 
             // Security check
@@ -940,7 +564,6 @@ document.addEventListener("DOMContentLoaded", async () => {
 
             if (Number.isFinite(id)) {
                 // Edit Mode
-                console.log("Modal in EDIT mode for alias ID:", id);
                 try {
                     await editAlias(id);
                 } catch (err) {
@@ -954,7 +577,6 @@ document.addEventListener("DOMContentLoaded", async () => {
                 }
             } else {
                 // Add Mode
-                console.log("Modal in CREATE mode");
                 clearAddAliasForm();
                 // Set focus to the first input field when modal is shown
                 const focusOnShown = () => {
@@ -972,7 +594,7 @@ document.addEventListener("DOMContentLoaded", async () => {
                 if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') {
                     lastTriggerEl.focus({ preventScroll: true });
                 } else {
-                    active.blur(); // fallback: evita warning A11Y
+                    active.blur();
                 }
             }
         });
@@ -986,37 +608,49 @@ document.addEventListener("DOMContentLoaded", async () => {
         });
     }
 
-    // 6) Button event delegation (click and keydown)
-    {
-        // Click event
-        document.addEventListener('click', async (e) => {
-            const el = e.target.closest('[data-action]');
-            if (!el) return;
-
-            const action = el.dataset.action;
-            const handler = actionHandlers[action];
-            if (!handler) return;
-
-            // Execute handler
-            try {
-                await handler(e, el);
-            } catch (err) {
-                console.error(err?.message || 'Action error', false);
-                showToast(err?.message || 'Action error', false);
-            }
-        });
+    // Button event delegation (click)
+    document.addEventListener('click', async (e) => {
+        const el = e.target.closest('[data-action]');
+        if (!el) return;
+
+        const action = el.dataset.action;
+        const handler = actionHandlers[action];
+        if (!handler) return;
 
-        // Keydown (Enter, Space) for accessibility
-        document.addEventListener('keydown', async (e) => {
-            const isEnter = e.key === 'Enter';
-            const isSpace = e.key === ' ' || e.key === 'Spacebar';
-            if (!isEnter && !isSpace) return;
+        // Execute handler
+        try {
+            await handler(e, el);
+        } catch (err) {
+            console.error(err?.message || 'Action error');
+            showToast(err?.message || 'Action error', false);
+        }
+    });
 
-            const el = e.target.closest('[data-action]');
-            if (!el) return;
+    // Button event delegation (Enter, Space)
+    document.addEventListener('keydown', async (e) => {
+        const isEnter = e.key === 'Enter';
+        const isSpace = e.key === ' ' || e.key === 'Spacebar';
+        if (!isEnter && !isSpace) return;
 
-            // Trigger click event
-            el.click();
-        });
+        const el = e.target.closest('[data-action]');
+        if (!el) return;
+
+        // Space/Enter
+        if (el.tagName === 'BUTTON') return;
+        // Trigger click event
+        el.click();
+    });
+
+    // Submit Form
+    const form = document.getElementById('addAliasForm');
+    if (form) {
+        form.addEventListener('submit', handleAddAliasSubmit);
     }
+
+    // Submit Sort
+    const headers = document.querySelectorAll('thead th');
+    headers.forEach((th) => {
+        if (th.dataset.sortable === 'false') return;
+        th.addEventListener('click', () => sortTable(th.cellIndex, sortState));
+    });
 });
diff --git a/frontend/js/common.js b/frontend/js/common.js
new file mode 100644 (file)
index 0000000..5b9ebd8
--- /dev/null
@@ -0,0 +1,307 @@
+// -----------------------------
+// Configuration parameters
+// -----------------------------
+let timeoutToast = 3000; // milliseconds
+const stringCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
+
+// -----------------------------
+// Validate the IPv4 address format
+// -----------------------------
+export 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}$/;
+    return ipv4.test(ip);
+}
+
+// -----------------------------
+// Validate the IPv6 address format
+// -----------------------------
+export 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));
+}
+
+// -----------------------------
+// Validate the MAC address format
+// -----------------------------
+export function isValidMAC(mac) {
+  const s = (mac ?? "").trim().toLowerCase().replace(/[\s:\-\.]/g, "");
+  // vuoto consentito
+  if (s === "") return true;
+  return /^[0-9a-f]{12}$/.test(s);
+}
+
+// -----------------------------
+// Display a temporary notification message
+// -----------------------------
+export function showToast(message, success = true) {
+    const toast = document.getElementById("toast");
+    toast.textContent = message;
+
+    toast.style.background = success ? "#28a745" : "#d9534f"; // green / red
+
+    toast.classList.add("show");
+
+    setTimeout(() => {
+        toast.classList.remove("show");
+    }, timeoutToast);
+}
+
+/**
+ * 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)
+ */
+export 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
+ */
+export 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
+ */
+export 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 <button>, puoi aggiornare aria-pressed/aria-label qui.
+    });
+}
+
+/**
+ * Sort the table by column
+ * @param {number} colIndex - Indice di colonna da ordinare (0-based)
+ * @param {{sortDirection: Record<number, boolean>, lastSort: {colIndex:number, ascending:boolean} | null}} state
+ * @param {string} [tableId='dataTable']
+ */
+export function sortTable(colIndex, state, tableId = 'dataTable') {
+  const table = document.getElementById(tableId);
+  if (!table) return;
+
+  const tbody = table.querySelector('tbody');
+  if (!tbody) return;
+
+  const rows = Array.from(tbody.querySelectorAll('tr'));
+  const ths = table.querySelectorAll('thead th');
+
+  // Tipo della colonna (fallback a string)
+  const type = ths[colIndex]?.dataset?.type || 'string';
+
+  // Toggle direzione (true = asc, false = desc)
+  state.sortDirection[colIndex] = !state.sortDirection[colIndex];
+  const ascending = !!state.sortDirection[colIndex];
+  const direction = ascending ? 1 : -1;
+
+  // Pre-parsing per performance
+  const parsed = rows.map((row, idx) => {
+    const cell = row.children[colIndex];
+    const raw = (cell?.getAttribute('data-value') ?? cell?.innerText ?? '').trim();
+    return { row, idx, parsed: parseByType(raw, type) };
+  });
+
+  // Sort con tie-break su indice originale per stabilità
+  parsed.sort((a, b) => {
+    const c = comparator(a.parsed, b.parsed);
+    return c !== 0 ? c * direction : (a.idx - b.idx);
+  });
+
+  // Re-append più efficiente
+  const frag = document.createDocumentFragment();
+  parsed.forEach(p => frag.appendChild(p.row));
+  tbody.appendChild(frag);
+
+  // UI/ARIA delle frecce
+  updateSortUI(table, colIndex, ascending);
+
+  // Aggiorna lo stato condiviso
+  state.lastSort = { colIndex, ascending };
+}
+
+/**
+ * Reset sorting state & UI
+ * @param {{sortDirection: Record<number, boolean>, lastSort: {colIndex:number, ascending:boolean} | null}} state
+ * @param {string} [tableId='dataTable']
+ */
+export function resetSorting(state, tableId = 'dataTable') {
+  const table = document.getElementById(tableId);
+  if (!table) return;
+
+  // Svuota sortDirection senza riassegnare (così il chiamante vede il cambiamento)
+  Object.keys(state.sortDirection).forEach(k => delete state.sortDirection[k]);
+  state.lastSort = null;
+
+  // Reset ARIA e classi frecce
+  table.querySelectorAll('thead th').forEach(th => {
+    th.setAttribute('aria-sort', 'none');
+    th.classList.remove('is-sorted');
+    const arrow = th.querySelector('.sort-arrow');
+    if (arrow) arrow.classList.remove('asc', 'desc');
+  });
+}
+
+/**
+ * Opzionale: inizializza aria-sort='none'
+ */
+export function initSortableTable() {
+    const table = document.getElementById("dataTable");
+    if (!table) return;
+    const ths = table.querySelectorAll("thead th");
+    ths.forEach(th => th.setAttribute("aria-sort", "none"));
+}
\ No newline at end of file
index 836b95c73936471aee9b2b89952181abaaba2184..6c3b54a6d9e21b9a623243443846add6cb7100c5 100644 (file)
@@ -1,56 +1,12 @@
-// -----------------------------
-// Configuration parameters
-// -----------------------------
-let timeoutToast = 3000; // milliseconds
+// Import common js
+import { isValidIPv4, isValidIPv6, isValidMAC, showToast, sortTable, initSortableTable, resetSorting } from './common.js';
+import { reloadDNS, reloadDHCP } from './services.js';
 
 // -----------------------------
 // State variables
 // -----------------------------
 let editingHostId = null;
-let sortDirection = {};
-let lastSort = null; // { colIndex: number, ascending: boolean }
-const stringCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
-
-// -----------------------------
-// Validate the IPv4 address format
-// -----------------------------
-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}$/;
-    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));
-}
-
-// -----------------------------
-// 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);
-}
+const sortState = { sortDirection: {}, lastSort: null };
 
 // -----------------------------
 // Load all hosts into the table
@@ -97,9 +53,9 @@ async function loadHosts() {
     }
 
     // DOM Reference
-    const tbody = document.querySelector("#hosts-table tbody");
+    const tbody = document.querySelector("#dataTable tbody");
     if (!tbody) {
-        console.warn('Element "#hosts-table tbody" not found in DOM.');
+        console.warn('Element "#dataTable tbody" not found in DOM.');
         return;
     }
 
@@ -528,136 +484,12 @@ async function handleDeleteHost(e, el) {
     return false;
 }
 
-// -----------------------------
-// Handle reload DNS action
-// -----------------------------
-async function handleReloadDNS() {
-    try {
-        // Fetch data
-        const res = await fetch(`/api/dns/reload`, {
-            method: 'POST',
-            headers: { 'Accept': 'application/json' },
-        });
-
-        // Success without JSON
-        if (res.status === 204) {
-            showToast('DNS reload successfully', true);
-            return true;
-        }
-
-        // Check content-type to avoid parsing errors
-        const contentType = res.headers.get("content-type") || "";
-        if (!contentType.includes("application/json")) {
-            const err = new Error(`${res.status}: ${res.statusText}`);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Check JSON
-        let data;
-        try {
-            data = await res.json();
-        } catch {
-            throw new Error('Invalid JSON payload');
-        }
-
-        // Check JSON errors
-        if (!res.ok) {
-            const serverMsg = data?.detail?.message?.trim();
-            const base = `Error reloading DNS`;
-            const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Success
-        showToast(data?.message || 'DNS reload successfully', true);
-        return true;
-
-    } catch (err) {
-        console.error(err?.message || "Error reloading DNS");
-        showToast(err?.message || "Error reloading DNS", false);
-    }
-
-    return false;
-}
-
-// -----------------------------
-// Handle reload DHCP action
-// -----------------------------
-async function handleReloadDHCP() {
-    try {
-        // Fetch data
-        const res = await fetch(`/api/dhcp/reload`, {
-            method: 'POST',
-            headers: { 'Accept': 'application/json' },
-        });
-
-        // Success without JSON
-        if (res.status === 204) {
-            showToast('DHCP reload successfully', true);
-            return true;
-        }
-
-        // Check content-type to avoid parsing errors
-        const contentType = res.headers.get("content-type") || "";
-        if (!contentType.includes("application/json")) {
-            const err = new Error(`${res.status}: ${res.statusText}`);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Check JSON
-        let data;
-        try {
-            data = await res.json();
-        } catch {
-            throw new Error('Error reloading DHCP: Invalid JSON payload');
-        }
-
-        // Check JSON errors
-        if (!res.ok) {
-            const serverMsg = data?.detail?.message?.trim();
-            const base = `Error reloadin DHCP`;
-            const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base);
-            err.status = res.status;
-            throw err;
-        }
-
-        // Success
-        showToast(data?.message || 'DHCP reload successfully', true);
-        return true;
-
-    } catch (err) {
-        console.error(err?.message || "Error reloading DHCP");
-        showToast(err?.message || "Error reloading DHCP", false);
-    }
-
-    return false;
-}
-
-// -----------------------------
-// Display a temporary notification message
-// -----------------------------
-function showToast(message, success = true) {
-    const toast = document.getElementById("toast");
-    toast.textContent = message;
-
-    toast.style.background = success ? "#28a745" : "#d9534f"; // green / red
-
-    toast.classList.add("show");
-
-    setTimeout(() => {
-        toast.classList.remove("show");
-    }, timeoutToast);
-}
-
 // -----------------------------
 // filter hosts in the table
 // -----------------------------
 function filterHosts() {
     const query = document.getElementById("searchInput").value.toLowerCase();
-    const rows = document.querySelectorAll("#hosts-table tbody tr");
+    const rows = document.querySelectorAll("#dataTable tbody tr");
 
     rows.forEach(row => {
         const text = row.textContent.toLowerCase();
@@ -675,258 +507,6 @@ async function clearSearch() {
     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 <button>, puoi aggiornare aria-pressed/aria-label qui.
-    });
-}
-
-// -----------------------------
-// Sort the table by column
-// -----------------------------
-function sortTable(colIndex) {
-    const table = document.getElementById("hosts-table");
-    if (!table) return;
-
-    const tbody = table.querySelector("tbody");
-    if (!tbody) return;
-
-    const rows = Array.from(tbody.querySelectorAll("tr"));
-    const ths = table.querySelectorAll("thead th");
-    const type = ths[colIndex]?.dataset?.type || "string";
-
-    // Toggle direction
-    sortDirection[colIndex] = !sortDirection[colIndex];
-    const ascending = !!sortDirection[colIndex];
-    const direction = ascending ? 1 : -1;
-
-    // Pre-parsing per performance
-    const parsed = rows.map((row, idx) => {
-        const cell = row.children[colIndex];
-        const raw = (cell?.getAttribute("data-value") ?? cell?.innerText ?? "").trim();
-        return { row, idx, parsed: parseByType(raw, type) };
-    });
-
-    parsed.sort((a, b) => {
-        const c = comparator(a.parsed, b.parsed);
-        return c !== 0 ? c * direction : (a.idx - b.idx); // tie-breaker
-    });
-
-    // Re-append in un DocumentFragment (più efficiente)
-    const frag = document.createDocumentFragment();
-    parsed.forEach(p => frag.appendChild(p.row));
-    tbody.appendChild(frag);
-
-    updateSortUI(table, colIndex, ascending);
-
-    lastSort = { colIndex, ascending };
-}
-
-/**
- * Opzionale: inizializza aria-sort='none'
- */
-function initSortableTable() {
-    const table = document.getElementById("hosts-table");
-    if (!table) return;
-    const ths = table.querySelectorAll("thead th");
-    ths.forEach(th => th.setAttribute("aria-sort", "none"));
-}
-
-// -----------------------------
-// Reset sorting arrows and directions
-// -----------------------------
-function resetSorting() {
-    // azzera lo stato
-    sortDirection = {};
-
-    const table = document.getElementById("hosts-table");
-    if (!table) return;
-
-    // reset ARIA e classi
-    table.querySelectorAll("thead th").forEach(th => {
-        th.setAttribute("aria-sort", "none");
-        th.classList.remove("is-sorted");
-        const arrow = th.querySelector(".sort-arrow");
-        if (arrow) arrow.classList.remove("asc", "desc");
-    });
-
-    lastSort = null;
-}
-
-// -----------------------------
-// RELOAD DNS
-// -----------------------------
-function reloadDNS() {
-    // Implement DNS reload logic here
-    showToast("DNS reloaded successfully");
-}
-
-// -----------------------------
-// RELOAD DHCP
-// -----------------------------
-function reloadDHCP() {
-    // Implement DHCP reload logic here
-    showToast("DHCP reloaded successfully");
-}
-
 // -----------------------------
 // Action Handlers
 // -----------------------------
@@ -939,28 +519,29 @@ const actionHandlers = {
   },
 
   // Reload DNS
-  async reloadDns()  { handleReloadDNS();  },
+  async reloadDns(e, el) { reloadDNS(); },
 
   // Reload DHCP
-  async reloadDhcp() { handleReloadDHCP(); },
+  async reloadDhcp(e, el) { reloadDHCP(); },
 };
 
 // -----------------------------
 // DOMContentLoaded: initialize everything
 // -----------------------------
 document.addEventListener("DOMContentLoaded", async () => {
-    // 1) Init UI sort (aria-sort, arrows)
+
+    // Init UI sort (aria-sort, arrows)
     initSortableTable();
 
-    // 2) Load data (hosts)
+    // Load data (hosts)
     try {
         await loadHosts();
     } catch (err) {
-        console.error("Error loading hosts:", err);
-        showToast("Error loading hosts:", false);
+        console.error(err?.message || "Error loading hosts");
+        showToast(err?.message || "Error loading hosts", false);
     }
 
-    // 3) search bar
+    // search bar
     const input = document.getElementById("searchInput");
     if (input) {
         // clean input on load
@@ -972,13 +553,14 @@ document.addEventListener("DOMContentLoaded", async () => {
             if (e.key === "Escape") {
                 e.preventDefault();     // evita side-effect (es. chiusure di modali del browser)
                 e.stopPropagation();    // evita che arrivi al listener globale
-                resetSorting();
+                resetSorting(sortState);
                 clearSearch();          // svuota input e ricarica tabella (come definito nella tua funzione)
+                filterHosts('');        // ripristina tabella
             }
         });
     }
 
-    // 4) global ESC key listener to clear search and reset sorting
+    // global ESC key listener to clear search and reset sorting
     document.addEventListener("keydown", (e) => {
         // Ignore if focus is in a typing field
         const tag = (e.target.tagName || "").toLowerCase();
@@ -988,12 +570,13 @@ document.addEventListener("DOMContentLoaded", async () => {
         if (e.key === "Escape" && !isTypingField) {
             // Prevent default form submission
             e.preventDefault();
-            resetSorting();
+            resetSorting(sortState);
             clearSearch();
+            filterHosts('');
         }
     });
 
-    // 5) Modal show/hidden events to prepare/reset the form
+    // Modal show/hidden events to prepare/reset the form
     const modalEl = document.getElementById('addHostModal');
     if (modalEl) {
 
@@ -1002,7 +585,7 @@ document.addEventListener("DOMContentLoaded", async () => {
 
         // When shown, determine Add or Edit mode
         modalEl.addEventListener('show.bs.modal', async (ev) => {
-            const lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit)
+            lastTriggerEl = ev.relatedTarget; // trigger (Add o Edit)
             const formEl = document.getElementById('addHostForm');
 
             // Security check
@@ -1014,7 +597,6 @@ document.addEventListener("DOMContentLoaded", async () => {
 
             if (Number.isFinite(id)) {
                 // Edit Mode
-                console.log("Modal in EDIT mode for host ID:", id);
                 try {
                     await editHost(id);
                 } catch (err) {
@@ -1028,7 +610,6 @@ document.addEventListener("DOMContentLoaded", async () => {
                 }
             } else {
                 // Add Mode
-                console.log("Modal in CREATE mode");
                 clearAddHostForm();
                 // Set focus to the first input field when modal is shown
                 const focusOnShown = () => {
@@ -1046,7 +627,7 @@ document.addEventListener("DOMContentLoaded", async () => {
                 if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') {
                     lastTriggerEl.focus({ preventScroll: true });
                 } else {
-                    active.blur(); // fallback: evita warning A11Y
+                    active.blur();
                 }
             }
         });
@@ -1060,37 +641,49 @@ document.addEventListener("DOMContentLoaded", async () => {
         });
     }
 
-    // 6) Button event delegation (click and keydown)
-    {
-        // Click event
-        document.addEventListener('click', async (e) => {
-            const el = e.target.closest('[data-action]');
-            if (!el) return;
-
-            const action = el.dataset.action;
-            const handler = actionHandlers[action];
-            if (!handler) return;
-
-            // Execute handler
-            try {
-                await handler(e, el);
-            } catch (err) {
-                console.error(err?.message || 'Action error', false);
-                showToast(err?.message || 'Action error', false);
-            }
-        });
+    // Button event delegation (click)
+    document.addEventListener('click', async (e) => {
+        const el = e.target.closest('[data-action]');
+        if (!el) return;
 
-        // Keydown (Enter, Space) for accessibility
-        document.addEventListener('keydown', async (e) => {
-            const isEnter = e.key === 'Enter';
-            const isSpace = e.key === ' ' || e.key === 'Spacebar';
-            if (!isEnter && !isSpace) return;
+        const action = el.dataset.action;
+        const handler = actionHandlers[action];
+        if (!handler) return;
 
-            const el = e.target.closest('[data-action]');
-            if (!el) return;
+        // Execute handler
+        try {
+            await handler(e, el);
+        } catch (err) {
+            console.error(err?.message || 'Action error');
+            showToast(err?.message || 'Action error', false);
+        }
+    });
 
-            // Trigger click event
-            el.click();
-        });
+    // Button event delegation (Enter, Space)
+    document.addEventListener('keydown', async (e) => {
+        const isEnter = e.key === 'Enter';
+        const isSpace = e.key === ' ' || e.key === 'Spacebar';
+        if (!isEnter && !isSpace) return;
+
+        const el = e.target.closest('[data-action]');
+        if (!el) return;
+
+        // Space/Enter
+        if (el.tagName === 'BUTTON') return;
+        // Trigger click event
+        el.click();
+    });
+
+    // Submit Form
+    const form = document.getElementById('addHostForm');
+    if (form) {
+        form.addEventListener('submit', handleAddHostSubmit);
     }
+
+    // Submit Sort
+    const headers = document.querySelectorAll('thead th');
+    headers.forEach((th) => {
+        if (th.dataset.sortable === 'false') return;
+        th.addEventListener('click', () => sortTable(th.cellIndex, sortState));
+    });
 });
index b031f050ff712cc5cc39c43cd1311846c8d5a2e7..0985646554ba43db8e227baecba67a057ce7bab0 100644 (file)
@@ -1,10 +1,10 @@
 document.addEventListener('DOMContentLoaded', () => {
     const form = document.getElementById('loginForm');
     if (!form) return;
-    
+
     // Un solo AbortController per submit in corso
     let inFlightController = null;
-    
+
     form.addEventListener('submit', async (e) => {
         // Prevent default form submission
         e.preventDefault();
@@ -42,7 +42,7 @@ document.addEventListener('DOMContentLoaded', () => {
             // in caso di credenziali errate, metti focus
             if (focus === 'username') userEl?.focus();
             else if (focus === 'password') passEl?.focus();
-            
+
             // aggiungi classe is-invalid se vuoi la resa Bootstrap
             if (markUser) userEl?.classList.add('is-invalid');
             if (markPass) passEl?.classList.add('is-invalid');
@@ -50,7 +50,7 @@ document.addEventListener('DOMContentLoaded', () => {
 
         // Pulizia
         hideError();
-    
+
         // Normalizza input
         const user = userEl?.value?.trim() ?? '';
         const pass = passEl?.value ?? '';
@@ -64,14 +64,14 @@ document.addEventListener('DOMContentLoaded', () => {
           });
           return;
         }
-       
+
         // Evita submit multipli
         if (btn?.disabled) return;
 
         // Annulla eventuale richiesta precedente
         inFlightController?.abort();
         inFlightController = new AbortController();
-    
+
         // Disabilita UI + spinner
         const originalBtnHTML = btn.innerHTML;
        if(btn) {
diff --git a/frontend/js/services.js b/frontend/js/services.js
new file mode 100644 (file)
index 0000000..1cf794e
--- /dev/null
@@ -0,0 +1,109 @@
+import { showToast } from './common.js';
+
+// -----------------------------
+// Reload DNS
+// -----------------------------
+export async function reloadDNS() {
+    try {
+        // Fetch data
+        const res = await fetch(`/api/dns/reload`, {
+            method: 'POST',
+            headers: { 'Accept': 'application/json' },
+        });
+
+        // Success without JSON
+        if (res.status === 204) {
+            showToast('DNS reload successfully', true);
+            return true;
+        }
+
+        // Check content-type to avoid parsing errors
+        const contentType = res.headers.get("content-type") || "";
+        if (!contentType.includes("application/json")) {
+            const err = new Error(`${res.status}: ${res.statusText}`);
+            err.status = res.status;
+            throw err;
+        }
+
+        // Check JSON
+        let data;
+        try {
+            data = await res.json();
+        } catch {
+            throw new Error('Invalid JSON payload');
+        }
+
+        // Check JSON errors
+        if (!res.ok) {
+            const serverMsg = data?.detail?.message?.trim();
+            const base = `Error reloading DNS`;
+            const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base);
+            err.status = res.status;
+            throw err;
+        }
+
+        // Success
+        showToast(data?.message || 'DNS reload successfully', true);
+        return true;
+
+    } catch (err) {
+        console.error(err?.message || "Error reloading DNS");
+        showToast(err?.message || "Error reloading DNS", false);
+    }
+
+    return false;
+}
+
+// -----------------------------
+// Reload DHCP action
+// -----------------------------
+export async function reloadDHCP() {
+    try {
+        // Fetch data
+        const res = await fetch(`/api/dhcp/reload`, {
+            method: 'POST',
+            headers: { 'Accept': 'application/json' },
+        });
+
+        // Success without JSON
+        if (res.status === 204) {
+            showToast('DHCP reload successfully', true);
+            return true;
+        }
+
+        // Check content-type to avoid parsing errors
+        const contentType = res.headers.get("content-type") || "";
+        if (!contentType.includes("application/json")) {
+            const err = new Error(`${res.status}: ${res.statusText}`);
+            err.status = res.status;
+            throw err;
+        }
+
+        // Check JSON
+        let data;
+        try {
+            data = await res.json();
+        } catch {
+            throw new Error('Error reloading DHCP: Invalid JSON payload');
+        }
+
+        // Check JSON errors
+        if (!res.ok) {
+            const serverMsg = data?.detail?.message?.trim();
+            const base = `Error reloadin DHCP`;
+            const err = new Error(serverMsg ? `${base}: ${serverMsg}` : base);
+            err.status = res.status;
+            throw err;
+        }
+
+        // Success
+        showToast(data?.message || 'DHCP reload successfully', true);
+        return true;
+
+    } catch (err) {
+        console.error(err?.message || "Error reloading DHCP");
+        showToast(err?.message || "Error reloading DHCP", false);
+    }
+
+    return false;
+}