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
# ------------------------------------------------------------------------------
# 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"))
# ---------------------------------------------------------
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
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
# 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"))
# ---------------------------------------------------------
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"
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"
-// -----------------------------
-// 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
}
// 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;
}
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);
}
}
}
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();
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
// -----------------------------
},
// 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
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();
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) {
// 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
if (Number.isFinite(id)) {
// Edit Mode
- console.log("Modal in EDIT mode for alias ID:", id);
try {
await editAlias(id);
} catch (err) {
}
} else {
// Add Mode
- console.log("Modal in CREATE mode");
clearAddAliasForm();
// Set focus to the first input field when modal is shown
const focusOnShown = () => {
if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') {
lastTriggerEl.focus({ preventScroll: true });
} else {
- active.blur(); // fallback: evita warning A11Y
+ active.blur();
}
}
});
});
}
- // 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));
+ });
});
--- /dev/null
+// -----------------------------
+// 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
-// -----------------------------
-// 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
}
// 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;
}
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();
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
// -----------------------------
},
// 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
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();
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) {
// 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
if (Number.isFinite(id)) {
// Edit Mode
- console.log("Modal in EDIT mode for host ID:", id);
try {
await editHost(id);
} catch (err) {
}
} else {
// Add Mode
- console.log("Modal in CREATE mode");
clearAddHostForm();
// Set focus to the first input field when modal is shown
const focusOnShown = () => {
if (lastTriggerEl && typeof lastTriggerEl.focus === 'function') {
lastTriggerEl.focus({ preventScroll: true });
} else {
- active.blur(); // fallback: evita warning A11Y
+ active.blur();
}
}
});
});
}
- // 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));
+ });
});
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();
// 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');
// Pulizia
hideError();
-
+
// Normalizza input
const user = userEl?.value?.trim() ?? '';
const pass = passEl?.value ?? '';
});
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) {
--- /dev/null
+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;
+}