]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added boostrap
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Mon, 26 Jan 2026 15:57:27 +0000 (16:57 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Mon, 26 Jan 2026 15:57:27 +0000 (16:57 +0100)
backend/routes/hosts.py
backend/routes/login.py
frontend/css/hosts.css [deleted file]
frontend/css/layout.css
frontend/css/login.css [deleted file]
frontend/css/variables.css
frontend/hosts.html
frontend/js/hosts.js
frontend/js/login.js
frontend/login.html

index 4337955291471af8b1bbcf7684198a4673d6d517..3a68e70936b1f7f5f7b9af40cc96cc9b8eedf324 100644 (file)
@@ -27,11 +27,6 @@ router = APIRouter()
 def hosts(request: Request):
     return FileResponse(os.path.join(settings.FRONTEND_DIR, "hosts.html"))
 
-# Serve hosts.css
-@router.get("/css/hosts.css")
-def css_hosts():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "css/hosts.css"))
-
 # Serve hosts.js
 @router.get("/js/hosts.js")
 def css_hosts():
index 4a8789bcda38cb333853d2157d85c8528aa60146..2b7a2246aa135db6426f1df8f4bd6a98baa3b4a9 100644 (file)
@@ -37,11 +37,6 @@ def check_rate_limit(ip: str):
 def login_page(request: Request):
     return FileResponse(os.path.join(settings.FRONTEND_DIR, "login.html"))
 
-# Serve login.css
-@router.get("/css/login.css")
-def css_login():
-    return FileResponse(os.path.join(settings.FRONTEND_DIR, "css/login.css"))
-
 # Serve login.js
 @router.get("/js/login.js")
 def css_login():
diff --git a/frontend/css/hosts.css b/frontend/css/hosts.css
deleted file mode 100644 (file)
index d9fb0a7..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/* ================================
-    Actions column
-   ================================ */
-td.actions {
-    white-space: nowrap;
-    text-align: left;
-}
-
-.actions span {
-    cursor: pointer;
-    display: inline-flex;
-    align-items: center;
-    padding: 4px;
-    border-radius: 4px;
-    transition: background 0.2s ease;
-    margin-right: 8px;
-}
-
-.actions span:hover {
-    background-color: #e0f0ff;
-}
-
-/* ================================
-   Search bar
-   ================================ */
-.search-bar {
-    width: 250px;
-    padding: 8px 12px;
-    border: 1px solid var(--border-light);
-    border-radius: 6px;
-    font-size: 14px;
-}
-
-/* ================================
-   Sort arrows
-   ================================ */
-.sort-arrow {
-    display: inline-block;
-    width: 1em;
-    text-align: center;
-    margin-left: .25rem;
-    color: var(--text-dark);
-}
-
-.sort-arrow.asc::after {
-    content: "▲";
-}
-
-.sort-arrow.desc::after {
-    content: "▼";
-}
index e3d0bd83721818c73d5f71ec10b1adc428d6d68f..381ad66128624db511929c9d64154dd30cbaf0ca 100644 (file)
@@ -12,6 +12,13 @@ body {
     margin: 20px;
     padding-top: 60px; /* space for fixed topbar */
     background-color: var(--bg-light);
+    color: var(--text-dark);
+}
+
+.body-login {
+    margin: 0;
+    padding-top: 0;
+    background: var(--bg-light);
 }
 
 /* ================================
@@ -20,9 +27,6 @@ body {
 .topbar {
     width: 100%;
     background: var(--bg-dark);
-    padding: 0;
-    display: flex;
-    align-items: center;
     border-bottom: 3px solid var(--accent);
     position: fixed;
     top: 0;
@@ -33,7 +37,7 @@ body {
 .topbar-inner {
     width: 100%;
     margin: 0 auto;
-    padding: 14px 26px;
+    padding: 10px 26px;
     display: flex;
     justify-content: space-between;
     align-items: center;
@@ -79,69 +83,211 @@ body {
 }
 
 /* ================================
-   Buttons (pfSense style)
+   Login page
    ================================ */
-.btn-primary {
-    background-color: var(--accent);
-    color: white;
-    border: none;
-    padding: 6px 14px;
-    font-size: 0.95rem;
+.login-wrapper {
+    min-height: 100vh;
+    min-height: 100svh; /* mobile moderni */
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 20px 0;
+    background: var(--bg-light);
+}
+
+.login-box {
+    background: #fff;
+    width: 100%;
+    max-width: 320px;
+    padding: 24px 28px;
+    border-radius: 8px;
+    border-left: 4px solid var(--accent);
+    box-shadow: 0 4px 14px var(--shadow-soft);
+}
+
+.login-box-header {
+    background: var(--bg-dark);
+    padding: 10px 14px;
+    margin: -24px -28px 16px -28px; /* stacca dal box e abbraccia i bordi */
+    border-radius: 8px 8px 0 0;
+    display: flex;
+    align-items: center;
+    gap: 10px;
+}
+
+.login-box-header span {
+    color: var(--text-light);
+    font-weight: 600;
+    font-size: 1rem;
+}
+
+.login-box h2 {
+    margin: 0 0 16px;
+    font-size: 1.2rem;
+    color: var(--text-dark);
+}
+
+.login-box .form-control {
+    background: var(--bg-light);
+}
+
+/*GRGR*/
+.placeholder-italic::placeholder {
+    font-style: italic;
+}
+
+/* ================================
+    Buttons
+   ================================ */
+.btn {
     font-weight: 600;
     border-radius: 4px;
-    cursor: pointer;
-    transition: background 0.2s ease, filter 0.2s ease;
     box-shadow: 0 2px 4px rgba(0,0,0,0.15);
+    border: none;
+    padding: 6px 14px;
+    font-size: 0.95rem;
+    line-height: 1;
+}
+
+.btn-primary {
+    background-color: var(--accent);
+    border-color: var(--accent);
+    color: #fff;
 }
 
-.btn-primary:hover {
+.btn-primary:hover,
+.btn-primary:focus {
     background-color: var(--accent-hover);
+    border-color: var(--accent-hover);
     filter: brightness(0.96);
 }
 
-.frame-row {
-    display: flex;
-    align-items: center;
-    gap: 12px;
+.btn-outline-primary {
+    color: var(--accent);
+    border-color: var(--accent);
+    background-color: transparent;
 }
 
-.frame-row h2 {
-    margin-right: auto;
+.btn-outline-primary:hover,
+.btn-outline-primary:focus {
+    color: #fff;
+    background-color: var(--accent);
+    border-color: var(--accent);
+}
+
+.btn-primary.btn-login {
+    width: 100%;
+    padding: 8px 0;
+    box-shadow: none;
+}
+
+.btn-primary.btn-login:hover,
+.btn-primary.btn-login:focus,
+.btn-primary.btn-login:active {
+    box-shadow: none;
+}
+
+.btn-primary.btn-login:disabled {
+    opacity: .65;
+    box-shadow: none;
+    filter: none;
 }
 
 /* ================================
-   Typography
+   Form controls
    ================================ */
-h1 {
-    margin-bottom: 20px;
+.form-control,
+.form-select {
+    border: 1px solid var(--border-light);
+    border-radius: 6px;
+    padding: 4px 8px;
+    font-size: 0.85rem;
+    height: auto;
+    line-height: 1.1;
+}
+
+.form-label {
+    font-size: 0.80rem;          /* più piccina */
+    margin-bottom: 2px;          /* Bootstrap usa 8px; lo riduciamo */
 }
 
 /* ================================
-   Table styling
+   Focus globale per controlli form
    ================================ */
-table {
-    border-collapse: collapse;
-    width: 100%;
-    background-color: white;
+input.form-control:focus,
+textarea.form-control:focus,
+select.form-select:focus,
+.form-control:focus,
+.form-select:focus,
+.form-check-input:focus {
+    border-color: var(--accent);
+    box-shadow: 0 0 0 1px var(--accent);
+    outline: none;
+}
+
+.form-control.is-invalid:focus,
+.form-select.is-invalid:focus {
+    box-shadow: none;
+    border-color: #dc3545; /* rosso Bootstrap */
+}
+
+.form-control.is-valid:focus,
+.form-select.is-valid:focus {
+    box-shadow: none;
+    border-color: #198754; /* verde Bootstrap */
+}
+
+.form-check-input:focus {
+    border-color: var(--accent);
+    box-shadow: 0 0 0 1px var(--accent);
+    outline: none;
+}
+
+/* ================================
+   Icons
+   ================================ */
+.icon {
+    color: var(--icon-color);
+    display: inline-flex;
+    align-items: center;
+}
+
+.icon:hover {
+    color: var(--icon-color-hover);
+}
+
+.icon-static {
+    --icon-color-hover: var(--icon-color);
+}
+
+.icon-action {
+    cursor: pointer;
+}
+
+/* ================================
+   Table
+   ================================ */
+.table {
+    background-color: #fff;
     box-shadow: 0 3px 6px rgba(0,0,0,0.18);
+    border-color: var(--border-light);
 }
 
-th, td {
-    border: 1px solid var(--border-light);
-    padding: 10px 12px;
-    text-align: left;
+.table th,
+.table td {
     vertical-align: middle;
+    border-color: var(--border-light);
 }
 
-th {
+.table thead th {
     background-color: #eee;
-    font-weight: bold;
+    font-weight: 700;
     cursor: pointer;
     user-select: none;
     position: relative;
 }
 
-th:hover::after {
+.table thead th:hover::after {
     content: "";
     position: absolute;
     left: 0;
@@ -151,17 +297,48 @@ th:hover::after {
     background: var(--accent);
 }
 
+/* Sort arrows */
 .sort-arrow {
-    margin-left: 6px;
-    font-size: 12px;
-    opacity: 0.6;
     display: inline-block;
-    width: 12px;
+    width: 1em;
     text-align: center;
+    margin-left: .25rem;
+    color: var(--text-dark);
+}
+
+.sort-arrow.asc::after {
+    content: "▲";
+}
+
+.sort-arrow.desc::after {
+    content: "▼";
 }
 
 /* ================================
-   Toast notification
+   Actions column
+   ================================ */
+td.actions {
+    white-space: nowrap;
+    text-align: left;
+}
+
+.actions span {
+    cursor: pointer;
+    display: inline-flex;
+    align-items: center;
+    padding: 4px;
+    border-radius: 4px;
+    transition: background 0.2s ease;
+    margin-right: 8px;
+}
+
+/* removed due to background with icons
+.actions span:hover {
+    background-color: #e0f0ff;
+}*/
+
+/* ================================
+   Toast
    ================================ */
 .toast {
     position: fixed;
@@ -232,31 +409,74 @@ th:hover::after {
     margin-top: 18px;
 }
 
-.cancel-btn {
-    background: #ccc;
-    border: none;
-    padding: 6px 12px;
-    border-radius: 4px;
-    cursor: pointer;
+/* ================================
+   Modal "Add Host"
+================================ */
+.addhost-modal {
+    border-left: 4px solid var(--accent);
+    border-radius: 8px;
+    box-shadow: 0 4px 12px var(--shadow-strong);
 }
 
-.save-btn {
-    background: var(--accent);
-    color: white;
-    border: none;
-    padding: 6px 14px;
-    border-radius: 4px;
-    cursor: pointer;
+.addhost-header {
+    background: var(--bg-dark);
+    color: var(--text-light);
+    border-bottom: none;               /* più pulita */
+    padding: 10px 14px;
 }
 
-.save-btn:hover {
-    background: var(--accent-hover);
+.addhost-header .modal-title {
+    font-weight: 600;
+    font-size: 1rem;                   /* compatto */
+}
+
+.addhost-header .btn-close {
+    filter: invert(1);                 /* icona chiara su bg scuro */
+    opacity: .85;
+}
+
+.addhost-header .btn-close:hover {
+    opacity: 1;
+}
+
+/* Body & footer compatti come il resto dell'app */
+.addhost-modal .modal-body {
+    padding: 14px 16px;
+}
+
+.addhost-modal .modal-footer {
+    border-top: none;
+    padding: 10px 14px 14px;
+}
+
+/* Label & inputs: usano già i tuoi override globali (form-control compatto) */
+.addhost-modal .form-label {
+    font-size: 0.80rem;
+    margin-bottom: 2px;
+}
+
+/* Title icon coerente con l'header */
+.addhost-header .title-icon {
+    font-size: 1.2em;
+    line-height: 1;
+    display: inline-flex;
+    transform: translateY(-1px);
 }
 
 /* ================================
-   SVG icons
+   Responsive
    ================================ */
-svg {
-    display: block;
-    pointer-events: none;
+@media (max-width: 640px) {
+    body {
+        padding-top: 48px; /* topbar più compatta su mobile */
+    }
+
+    /* Toolbar: su schermi molto stretti mostra solo le icone dei bottoni */
+    .btn .label {
+        display: none;
+    }
+
+    .btn .icon {
+        margin-right: 0;
+    }
 }
diff --git a/frontend/css/login.css b/frontend/css/login.css
deleted file mode 100644 (file)
index 03e66c2..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-/* ================================
-   Body styles for login page
-   ================================ */
-
-/* Variante per login */
-.body-login {
-    margin: 0;
-    padding-top: 0;
-}
-
-/* ================================
-   Login page wrapper
-   ================================ */
-.login-wrapper {
-    min-height: 100vh;
-    min-height: 100svh;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    background: var(--bg-light);
-    overflow-y: auto;
-    padding: 20px 0;
-}
-
-/* ================================
-   Login box
-   ================================ */
-.login-box {
-    background: white;
-    padding: 24px 28px;
-    border-radius: 8px;
-    width: 320px;
-    box-shadow: 0 4px 14px var(--shadow-soft);
-    border-left: 4px solid var(--accent);
-}
-
-/* ================================
-   Login box header
-   ================================ */
-.login-box-header {
-    background: var(--bg-dark);
-    padding: 10px 14px;
-    margin: -24px -28px 16px -28px;
-    border-radius: 8px 8px 0 0;
-    display: flex;
-    align-items: center;
-    gap: 10px;
-}
-
-.login-box-header span {
-    color: var(--text-light);
-    font-weight: 600;
-    font-size: 1rem;
-}
-
-/* ================================
-   Logo / title
-   ================================ */
-.login-logo {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    margin-bottom: 12px;
-    color: var(--accent);
-    font-weight: 600;
-}
-
-.login-box h2 {
-    margin: 0 0 16px;
-    font-size: 1.2rem;
-    color: var(--text-dark);
-}
-
-/* ================================
-   Labels & inputs
-   ================================ */
-.login-box label {
-    display: block;
-    font-size: 0.85rem;
-    margin-bottom: 4px;
-    color: var(--text-dark);
-}
-
-.login-box input[type="text"],
-.login-box input[type="password"] {
-    width: 100%;
-    padding: 7px 10px;
-    border-radius: 4px;
-    border: 1px solid var(--border-light);
-    font-size: 0.95rem;
-    margin-bottom: 12px;
-    background: var(--bg-light);
-}
-
-/* ================================
-   Login button
-   ================================ */
-.btn-primary.login-btn {
-    width: 100%;
-    padding: 8px 0;
-    box-shadow: none;
-}
-
-.btn-primary.login-btn:hover {
-    box-shadow: none;
-}
-
-/* ================================
-   Error message
-   ================================ */
-.login-error {
-    margin-top: 10px;
-    font-size: 0.85rem;
-    color: #d9534f;
-    min-height: 16px;
-}
\ No newline at end of file
index 6852e31e472eb67006328e89a06d0bd0713ca8dc..2b1d835e26bf05669d4d6f5df3a8a5dc5c336fc4 100644 (file)
@@ -17,4 +17,7 @@
 
     --shadow-soft: rgba(0, 0, 0, 0.12);
     --shadow-strong: rgba(0, 0, 0, 0.25);
+
+    --icon-color: var(--accent);
+    --icon-color-hover: var(--accent-hover);
 }
index aab6edc1832b2ebb01af4c57776a0a15d3b1ce84..deb7807634a08d2acc4879a1628bf2a13cdc7d46 100644 (file)
     <meta charset="UTF-8">
     <title>Network Manager</title>
     <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <!-- Bootstrap 5.x CSS (CDN) -->
+    <link
+        href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
+        rel="stylesheet"
+        integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
+        crossorigin="anonymous"
+    >
+    <!-- Bootstrap Icons -->
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
+
+    <!-- Boostrap override -->
     <link rel="stylesheet" href="css/variables.css">
     <link rel="stylesheet" href="css/layout.css">
-    <link rel="stylesheet" href="css/hosts.css">
 </head>
-<body>
 
-<header class="topbar">
-    <div class="topbar-inner">
-        <div class="logo">
-            <svg width="30" height="30" viewBox="0 0 24 24" fill="var(--accent)">
-                <circle cx="12" cy="4" r="2"/>
-                <circle cx="4" cy="12" r="2"/>
-                <circle cx="20" cy="12" r="2"/>
-                <circle cx="12" cy="20" r="2"/>
-                <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"/>
-                <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"/>
-            </svg>
-            <span>Network Manager</span>
+<body>
+    <!-- Topbar -->
+    <header class="topbar">
+        <div class="topbar-inner">
+            <div class="logo">
+                <svg width="30" height="30" viewBox="0 0 24 24" fill="var(--accent)" aria-hidden="true">
+                    <circle cx="12" cy="4" r="2"></circle>
+                    <circle cx="4" cy="12" r="2"></circle>
+                    <circle cx="20" cy="12" r="2"></circle>
+                    <circle cx="12" cy="20" r="2"></circle>
+                    <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
+                    <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
+                </svg>
+                <span>Network Manager</span>
+            </div>
+
+            <button id="logoutBtn" class="btn btn-primary">Logout</button>
         </div>
-
-        <button id="logoutBtn" class="btn-primary">Logout</button>
-    </div>
-</header>
-
-<div id="toast" class="toast"></div>
-
-<section class="page-frame">
-    <div class="frame-row">
-        <h2><span class="section-title">🖧 Host List</span></h2>
-       
-        <input 
-            type="text" 
-            id="searchInput" 
-            placeholder="Ricerca..." 
-            oninput="filterHosts()" 
-            class="search-bar"
-        />
-
-        <button class="btn-primary" title="Add Host" aria-label="Add Host" onclick="openAddHostModal()">
-            + Add Host
-        </button>
-
-        <button class="btn-primary" title="Reload DNS (BIND)" aria-label="Reload DNS" onclick="reloadDNS()">
-            ↻ Reload DNS
-        </button>
-
-        <button class="btn-primary" title="Reload DHCP (Kea)" aria-label="Reload DHCP" onclick="reloadDHCP()">
-            ↻ Reload DHCP
-        </button>
-    </div>
-</section>
-
-<table id="hosts-table">
-    <thead>
-        <tr>
-            <th data-type="string" onclick="sortTable(0)">Name <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>
-        </tr>
-    </thead>
-    <tbody></tbody>
-</table>
-
-<!-- Popup Add Host -->
-<div id="addHostModal" class="modal">
-    <div class="modal-content">
-        <h3>Aggiungi Host</h3>
-
-        <label>Nome</label>
-        <input type="text" id="hostName">
-
-        <label>IPv4</label>
-        <input type="text" id="hostIPv4">
-
-        <label>IPv6</label>
-        <input type="text" id="hostIPv6">
-
-        <label>MAC Address</label>
-        <input type="text" id="hostMAC">
-
-        <label>Note</label>
-        <input type="text" id="hostNote">
-
-        <label>SSL?</label>
-        <input type="checkbox" id="hostSSL">
-
-        <div class="modal-buttons">
-            <button class="cancel-btn" onclick="closeAddHostModal()">Annulla</button>
-            <button class="save-btn" onclick="saveHost()">Salva</button>
+    </header>
+
+    <!-- Toast -->
+    <div id="toast" class="toast" role="status" aria-live="polite" aria-atomic="true"></div>
+
+    <!-- Toolbar / Section header -->
+    <section class="page-frame">
+        <div class="container-fluid p-0">
+            <div class="row g-2 align-items-center">
+                <!-- Title -->
+                <div class="col-12 col-md-auto">
+                    <h2 class="mb-0 d-flex align-items-center gap-2 lh-1">
+                        <span class="title-icon">🖧</span>
+                        <!--<i class="bi bi-hdd-network"></i>-->
+                        <span class="section-title">Host List</span>
+                    </h2>
+                </div>
+
+                <!-- Spacer -->
+                <div class="col d-none d-md-block"></div>
+
+                <!-- Search -->
+                <div class="col-12 col-md-auto">
+                    <div class="search-wrapper">
+                        <input
+                            type="text"
+                            id="searchInput"
+                            placeholder="Ricerca..."
+                            oninput="filterHosts()"
+                            class="form-control form-control-sm"
+                            aria-label="Search hosts"
+                        >
+                    </div>
+                </div>
+
+                <!-- Bottoni -->
+                <div class="col-12 col-md-auto d-flex gap-2 flex-wrap">
+                    <button class="btn btn-primary" title="Add Host" aria-label="Add Host" data-bs-toggle="modal" data-bs-target="#addHostModal">
+                        <i class="bi bi-plus-lg"></i><span class="label">Add Host</span>
+                    </button>
+                    <button class="btn btn-primary" title="Reload DNS (BIND)" aria-label="Reload DNS" onclick="reloadDNS()">
+                        <i class="bi bi-arrow-repeat"></i><span class="label">Reload DNS</span>
+                    </button>
+                    <button class="btn btn-primary" title="Reload DHCP (Kea)" aria-label="Reload DHCP" onclick="reloadDHCP()">
+                        <i class="bi bi-arrow-repeat"></i><span class="label">Reload DHCP</span>
+                    </button>
+                </div>
+            </div>
+        </div>
+    </section>
+
+    <!-- Tabella -->
+    <table id="hosts-table" class="table table-bordered table-hover align-middle">
+        <thead class="table-light">
+            <tr>
+                <th data-type="string" onclick="sortTable(0)">Name <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>
+            </tr>
+        </thead>
+        <tbody></tbody>
+    </table>
+
+    <div class="modal fade" id="addHostModal" tabindex="-1" aria-labelledby="addHostTitle" aria-hidden="true">
+        <div class="modal-dialog modal-dialog-centered"><!-- modal-sm|md|lg se vuoi cambiare -->
+            <div class="modal-content addhost-modal">
+                <!-- Header scuro con logo/brand -->
+                <div class="modal-header addhost-header">
+                    <div class="d-flex align-items-center gap-2">
+                        <!-- Emoji o icona -->
+                        <span class="title-icon" aria-hidden="true">🖧</span>
+                        <h5 class="modal-title mb-0" id="addHostTitle">Aggiungi Host</h5>
+                    </div>
+                    <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Chiudi"></button>
+                </div>
+
+                <div class="modal-body">
+                    <form id="addHostForm" onsubmit="return handleAddHostSubmit(event)">
+                        <div class="mb-2">
+                            <label for="hostName" class="form-label">Nome</label>
+                            <input type="text" id="hostName" class="form-control" required>
+                        </div>
+
+                        <!--<div class="row g-2">
+                            <div class="col-12 col-md-6">
+                                <label for="hostIPv4" class="form-label">IPv4</label>
+                                <input type="text" id="hostIPv4" class="form-control" inputmode="decimal" placeholder="es. 192.168.1.10">
+                            </div>
+                            <div class="col-12 col-md-6">
+                                <label for="hostIPv6" class="form-label">IPv6</label>
+                                <input type="text" id="hostIPv6" class="form-control" placeholder="es. fe80::1">
+                            </div>
+                        </div>-->
+
+                        <div class="mb-2">
+                            <label for="hostIPv4" class="form-label">IPv4</label>
+                            <input type="text" id="hostIPv4" class="form-control" inputmode="decimal" placeholder="es. 192.168.1.10">
+                        </div>
+                        <div class="mb-2">
+                            <label for="hostIPv6" class="form-label">IPv6</label>
+                            <input type="text" id="hostIPv6" class="form-control" placeholder="es. fe80::1">
+                        </div>
+
+
+                        <div class="mb-2">
+                            <label for="hostMAC" class="form-label">MAC Address</label>
+                            <input type="text" id="hostMAC" class="form-control" placeholder="es. AA:BB:CC:DD:EE:FF">
+                        </div>
+
+                        <div class="mb-2">
+                            <label for="hostNote" class="form-label">Note</label>
+                            <input type="text" id="hostNote" class="form-control">
+                        </div>
+
+                        <div class="form-check my-2">
+                            <input class="form-check-input" type="checkbox" id="hostSSL">
+                            <label class="form-check-label" for="hostSSL">SSL?</label>
+                        </div>
+                    </form>
+                </div>
+
+                <div class="modal-footer">
+                    <button type="submit" form="addHostForm" class="btn btn-primary">
+                        <i class="bi bi-check2"></i>
+                    </button>
+                    <button type="button" class="btn btn-primary" data-bs-dismiss="modal">
+                        <i class="bi bi-x"></i>
+                    </button>
+                </div>
+            </div>
         </div>
     </div>
-</div>
 
-<script src="js/hosts.js"></script>
-<script src="js/session.js"></script>
+    <!-- Scripts -->
+    <script src="js/hosts.js"></script>
+    <script src="js/session.js"></script>
 
+    <!-- Bootstrap JS -->
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
+            integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
+            crossorigin="anonymous"></script>
 </body>
 </html>
index aa757e0c01ebf84b2a5c398e478d85848ebbcb6e..2075561a289dc833c7eb1a34812285afb204f47b 100644 (file)
@@ -1,3 +1,11 @@
+// -----------------------------
+// Configuration parameters
+// -----------------------------
+let timeoutToast = 3000; // milliseconds
+
+// -----------------------------
+// State variables
+// -----------------------------
 let editingHostId = null;
 let sortDirection = {};
 let lastSort = null; // { colIndex: number, ascending: boolean }
@@ -70,21 +78,21 @@ async function loadHosts() {
 
         // IPv4
         const tdIPv4 = document.createElement("td");
-        const ipv4Raw = (h.ipv4 ?? "").trim();
+        const ipv4Raw = (h.ipv4 ?? "").toString().trim();
         tdIPv4.textContent = ipv4Raw;
         if (ipv4Raw) tdIPv4.setAttribute("data-value", ipv4Raw);
         tr.appendChild(tdIPv4);
 
         // IPv6
         const tdIPv6 = document.createElement("td");
-        const ipv6Raw = (h.ipv6 ?? "").trim();
+        const ipv6Raw = (h.ipv6 ?? "").toString().trim();
         tdIPv6.textContent = ipv6Raw;
         if (ipv6Raw) tdIPv6.setAttribute("data-value", ipv6Raw.toLowerCase());
         tr.appendChild(tdIPv6);
 
         // MAC
         const tdMAC = document.createElement("td");
-        const macRaw = (h.mac ?? "").trim();
+        const macRaw = (h.mac ?? "").toString().trim();
         tdMAC.textContent = macRaw;
         const macNorm = macRaw.toLowerCase().replace(/[\s:\-\.]/g, "");
         if (macNorm) tdMAC.setAttribute("data-value", macNorm);
@@ -99,33 +107,34 @@ async function loadHosts() {
 
         // SSL (icon)
         const tdSSL = document.createElement("td");
-        const sslEnabled = !!h.ssl_enabled; // 1/true -> true
+        const sslEnabled = !!h.ssl_enabled;
+        tdSSL.setAttribute("data-value", sslEnabled ? "true" : "false");
+        tdSSL.setAttribute("aria-label", sslEnabled ? "SSL attivo" : "SSL non attivo");
         if (sslEnabled) {
-            tdSSL.innerHTML = "&#10004;";
-            tdSSL.setAttribute("data-value", "true");
-            tdSSL.setAttribute("aria-label", "SSL attivo");
-        } else {
-            tdSSL.setAttribute("data-value", "false");
-            tdSSL.setAttribute("aria-label", "SSL non attivo");
+            tdSSL.innerHTML = '<i class="bi bi-shield-lock-fill icon icon-static" aria-hidden="true"></i>';
         }
         tr.appendChild(tdSSL);
 
         // Actions
         const tdActions = document.createElement("td");
         tdActions.className = "actions";
+
+        const id = Number(h.id);
+
         tdActions.innerHTML = `
-            <span class="edit-btn" onclick="editHost(${Number(h.id)})">
-                <svg width="18" height="18" viewBox="0 0 24 24" fill="#007BFF" aria-hidden="true">
-                    <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 
-                    7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 
-                    0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
-                </svg>
+            <span class="action-icon btn-edit ms-1"
+                    role="button" tabindex="0"
+                    title="Edit host" aria-label="Edit host"
+                    data-bs-toggle="modal" data-bs-target="#addHostModal" 
+                    data-host-id="${id}">
+                <i class="bi bi-pencil-fill icon icon-action" aria-hidden="true"></i>
             </span>
-            <span class="delete-btn" onclick="deleteHost(${Number(h.id)})">
-                <svg width="18" height="18" viewBox="0 0 24 24" fill="#0099FF" aria-hidden="true">
-                    <path d="M3 6h18v2H3V6zm2 3h14l-1.5 
-                    12.5h-11L5 9zm5-6h4l1 1h5v2H4V4h5l1-1z"/>
-                </svg>
+            <span class="action-icon btn-delete ms-1" 
+                    role="button" tabindex="0"
+                    title="Delete host" aria-label="Delete host"
+                    aria-label="Delete host"
+                    data-host-id="${id}">
+                <i class="bi bi-trash-fill icon icon-action" aria-hidden="true"></i>
             </span>
         `;
         tr.appendChild(tdActions);
@@ -140,89 +149,55 @@ async function loadHosts() {
 }
 
 // -----------------------------
-// OPEN POPUP IN EDIT MODE
+// Edit HOST: load data and pre-fill the form
 // -----------------------------
 async function editHost(id) {
-    const res = await fetch(`/api/hosts/${id}`);
-    if (!res.ok) {
-        console.error(`Errore nel recupero host ${id}:`, res.status);
-        showToast("Errore nel recupero host", false);
-        return;
-    }
-    const host = await res.json();
-
-    // Store the ID of the host being edited
-    editingHostId = id;
-
-    // Pre-fill the form fields
-    document.getElementById("hostName").value = host.name;
-    document.getElementById("hostIPv4").value = host.ipv4 || "";
-    document.getElementById("hostIPv6").value = host.ipv6 || "";
-    document.getElementById("hostMAC").value = host.mac || "";
-    document.getElementById("hostNote").value = host.note || "";
-    document.getElementById("hostSSL").checked = !!host.ssl_enabled;
+    try {
+        const res = await fetch(`/api/hosts/${id}`);
+        if (!res.ok) throw new Error(`Fetch failed for host ${id}: ${res.status}`);
 
-    document.getElementById("addHostModal").style.display = "flex";
-}
+        const host = await res.json();
 
-// -----------------------------
-// OPEN POPUP IN CREATE MODE
-// -----------------------------
-function openAddHostModal() {
-    editingHostId = null; // Reset edit mode
+        // Store the ID of the host being edited
+        editingHostId = id;
 
-    // Clear all fields
-    document.getElementById("hostName").value = "";
-    document.getElementById("hostIPv4").value = "";
-    document.getElementById("hostIPv6").value = "";
-    document.getElementById("hostMAC").value = "";
-    document.getElementById("hostNote").value = "";
-    document.getElementById("hostSSL").checked = false;
+        // Pre-fill the form fields
+        document.getElementById("hostName").value = host.name ?? "";
+        document.getElementById("hostIPv4").value = host.ipv4 ?? "";
+        document.getElementById("hostIPv6").value = host.ipv6 ?? "";
+        document.getElementById("hostMAC").value = host.mac ?? "";
+        document.getElementById("hostNote").value = host.note ?? "";
+        document.getElementById("hostSSL").checked = !!host.ssl_enabled;
 
-    document.getElementById("addHostModal").style.display = "flex";
-}
-
-// -----------------------------
-// CLOSE POPUP
-// -----------------------------
-function closeAddHostModal() {
-    editingHostId = null; // Always reset edit mode
-    document.getElementById("addHostModal").style.display = "none";
+    }  catch(err) {
+        throw err;
+    }
 }
 
 // -----------------------------
 // SAVE HOST (CREATE OR UPDATE)
 // -----------------------------
-async function saveHost() {
+async function saveHost(hostData) {
     // Validate required fields
-    if (!document.getElementById("hostName").value.trim()) {
+    if (!hostData.name.trim()) {
         showToast("Name is required", false);
-        return; // stop here, do NOT send the request
+        return false;
     }
     // Validate IPv4 format
-    if (!isValidIPv4(document.getElementById("hostIPv4").value)) {
+    if (!isValidIPv4(hostData.ipv4)) {
         showToast("Invalid IPv4 format", false);
-        return;
+        return false;
     }
     // Validate IPv6 format
-    if (!isValidIPv6(document.getElementById("hostIPv6").value)) {
+    if (!isValidIPv6(hostData.ipv6)) {
         showToast("Invalid IPv6 format", false);
-        return;
+        return false;
     }
     // Validate MAC format
-    if (!isValidMAC(document.getElementById("hostMAC").value)) {
+    if (!isValidMAC(hostData.mac)) {
         showToast("Invalid MAC format", false);
-        return;
-    }
-
-    const payload = {
-        name: document.getElementById("hostName").value,
-        ipv4: document.getElementById("hostIPv4").value,
-        ipv6: document.getElementById("hostIPv6").value,
-        mac: document.getElementById("hostMAC").value,
-        note: document.getElementById("hostNote").value,
-        ssl_enabled: document.getElementById("hostSSL").checked ? 1 : 0
-    };
+        return false;
+    } 
 
     try {
         if (editingHostId !== null) {
@@ -230,28 +205,31 @@ async function saveHost() {
             const res = await fetch(`/api/hosts/${editingHostId}`, {
                 method: "PUT",
                 headers: { "Content-Type": "application/json" },
-                body: JSON.stringify(payload)
+                body: JSON.stringify(hostData)
             });
-            if (!res.ok) throw new Error(`Update failed: ${res.status}`);
-            showToast("Host updated successfully");
+            if (res.ok) { 
+                showToast("Host updated successfully");
+            } else {
+                throw new Error(`Update failed: ${res.status}`);
+            }
         } else {
             // CREATE NEW HOST
             const res = await fetch("/api/hosts", {
                 method: "POST",
                 headers: { "Content-Type": "application/json" },
-                body: JSON.stringify(payload)
+                body: JSON.stringify(hostData)
             });
-            if (!res.ok) throw new Error(`Create failed: ${res.status}`);
-            showToast("Host added successfully");
+            if (res.ok) {
+                showToast("Host added successfully", true);
+            } else {
+                throw new Error(`Create failed: ${res.status}`);
+            }
         }
-
-        closeAddHostModal();
-        await loadHosts();
-
     } catch (err) {
-        console.error(err);
-        showToast("Error while saving host", false);
+        console.error("Error saving host:", err);
+        throw err;
     }
+    return true;
 }
 
 // -----------------------------
@@ -266,13 +244,84 @@ async function deleteHost(id) {
         showToast("Host removed successfully");
 
     } catch (err) {
-        console.error(err);
-        showToast("Error while removing host", false);
+        console.error("Error deleting host:", err);
+        throw err;
     }
-
     await loadHosts();
 }
 
+// -----------------------------
+// PREPARE ADD HOST FORM
+// -----------------------------
+function prepareAddHostForm() {
+    // reset edit mode
+    editingHostId = null;
+    // reset form fields
+    document.getElementById('addHostForm')?.reset();
+    console.log("Add Host form reset");
+}
+
+// -----------------------------
+// CLOSE POPUP
+// -----------------------------
+async function closeAddHostModal() {
+    const modalEl = document.getElementById('addHostModal');
+    const modal = bootstrap.Modal.getInstance(modalEl) 
+            || bootstrap.Modal.getOrCreateInstance(modalEl);
+    modal.hide();
+}
+
+// -----------------------------
+// Handle Add Host Form Submit
+// -----------------------------
+async function handleAddHostSubmit(e) {
+    e.preventDefault();
+    // Leggi i campi
+    const hostData = {
+        name:  document.getElementById('hostName').value.trim(),
+        ipv4:  document.getElementById('hostIPv4').value.trim(),
+        ipv6:  document.getElementById('hostIPv6').value.trim(),
+        mac:   document.getElementById('hostMAC').value.trim(),
+        note:  document.getElementById('hostNote').value.trim(),
+        ssl_enabled: document.getElementById('hostSSL').checked ? 1 : 0
+    };
+
+    try {
+        const ok = await saveHost(hostData);
+        if (ok !== false) {
+            // chiudi modale e ricarica tabella
+            closeAddHostModal();
+            await loadHosts();
+        }
+    } catch (err) {
+        console.error("Error saving host:", err);
+        showToast("Error saving host", false);
+    }
+    return false;
+}
+
+async function handleDeleteHost(e) {
+    const btn = e.target.closest('.btn-delete');
+    if (!btn) return;
+
+    e.preventDefault();
+    const idAttr = btn.getAttribute('data-host-id');
+    const id = idAttr ? Number(idAttr) : NaN;
+    if (!Number.isFinite(id)) {
+        console.warn('Delete: host id not valid for delete:', idAttr);
+        showToast('Host id not valid for delete', false);
+        return;
+    }
+
+    // Execute delete
+    try {
+        deleteHost(id);
+    } catch (err) {
+        console.error("Error deleting host:", err);
+        showToast("Error deleting host", false);
+    }
+}
+
 // -----------------------------
 // Display a temporary notification message
 // -----------------------------
@@ -286,7 +335,7 @@ function showToast(message, success = true) {
 
     setTimeout(() => {
         toast.classList.remove("show");
-    }, 2500);
+    }, timeoutToast);
 }
 
 // -----------------------------
@@ -565,7 +614,7 @@ function reloadDHCP() {
 }
 
 // -----------------------------
-// INITIAL TABLE LOAD
+// DOMContentLoaded: initialize everything
 // -----------------------------
 document.addEventListener("DOMContentLoaded", async () => {
     // 1) Init UI sort (aria-sort, arrows)
@@ -575,8 +624,8 @@ document.addEventListener("DOMContentLoaded", async () => {
     try {
         await loadHosts();
     } catch (err) {
-        console.error("Errore nel caricamento degli host:", err);
-        showToast("Errore nel caricamento degli host", false);
+        console.error("Error loading hosts:", err);
+        showToast("Error loading hosts:", false);
     }
 
     // 3) search bar
@@ -584,10 +633,8 @@ document.addEventListener("DOMContentLoaded", async () => {
     if (input) {
         // clean input on load
         input.value = "";
-
         // live filter for each keystroke
         input.addEventListener("input", filterHosts);
-
         // Escape management when focus is in the input
         input.addEventListener("keydown", (e) => {
             if (e.key === "Escape") {
@@ -597,13 +644,14 @@ document.addEventListener("DOMContentLoaded", async () => {
                 clearSearch();          // svuota input e ricarica tabella (come definito nella tua funzione)
             }
         });
-  }
+    }
 
     // 4) 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();
-        const isTypingField = tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable;
+        const isTypingField = 
+            tag === "input" || tag === "textarea" || tag === "select" || e.target.isContentEditable;
 
         if (e.key === "Escape" && !isTypingField) {
             e.preventDefault();
@@ -611,5 +659,81 @@ document.addEventListener("DOMContentLoaded", async () => {
             clearSearch();
         }
     });
-});
 
+    // 5) 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 triggerEl = ev.relatedTarget; // trigger (Add o Edit)
+            const formEl = document.getElementById('addHostForm');
+
+            // Security check
+            if (!formEl) return;
+
+            // check Add or Edit mode
+            const idAttr = triggerEl?.getAttribute?.('data-host-id');
+            const id = idAttr ? Number(idAttr) : null;
+            
+            if (Number.isFinite(id)) {  
+                // Edit Mode
+                console.log("Modal in EDIT mode for host ID:", id);
+                try {
+                    await editHost(id);
+                } catch (err) {
+                    console.error("Error loading host for edit:", err);
+                    showToast("Error loading host for edit", false);
+                    // Close modal
+                    const closeOnShown = () => {
+                        closeAddHostModal();
+                    };
+                    modalEl.addEventListener('shown.bs.modal', closeOnShown);
+                }
+            } else {
+                // Add Mode
+                console.log("Modal in CREATE mode");
+                prepareAddHostForm();
+                // Set focus to the first input field when modal is shown
+                const focusOnShown = () => {
+                    document.getElementById('hostName')?.focus({ preventScroll: true });
+                    modalEl.removeEventListener('shown.bs.modal', focusOnShown);
+                };
+                modalEl.addEventListener('shown.bs.modal', focusOnShown);
+            }
+        });
+
+        // When hidden, reset the form
+        modalEl.addEventListener('hidden.bs.modal', () => {
+            //prepareAddHostForm();
+        });
+    }
+
+    // 6) Delete button event delegation (click and keydown)
+    {
+        // Click event
+        document.addEventListener('click', (e) => {
+            // Execute delete
+            try {
+                handleDeleteHost(e);
+            } catch (err) {
+                console.error("Error deleting host:", err);
+                showToast("Error deleting host", false);
+            }
+        });
+
+        // Keydown (Enter, Space) for accessibility
+        document.addEventListener('keydown', (e) => {
+            const isEnter = e.key === 'Enter';
+            const isSpace = e.key === ' ' || e.key === 'Spacebar';
+            if (!isEnter && !isSpace) return;
+
+            // Execute delete
+            try {
+                handleDeleteHost(e);
+            } catch (err) {
+                console.error("Error deleting host:", err);
+                showToast("Error deleting host", false);
+            }
+        });
+    }
+});
index 3b1305351b73e38e4a7c2689f70ec914762a3e98..37abe33d07badcb65dacd5f1de5e3b1c500267f2 100644 (file)
+
 // -----------------------------
-// Login function
+// Login function (UX migliorata)
 // -----------------------------
 async function handleLogin(e) {
     e.preventDefault();
 
-    const user = document.getElementById("username").value.trim();
-    const pass = document.getElementById("password").value;
-
-    const res = await fetch("/api/login", {
-        method: "POST",
-        headers: { "Content-Type": "application/json" },
-        credentials: "include",
-        body: JSON.stringify({
-            username: user,
-            password: pass
-        })
-    });
-
-    const data = await res.json();
-
-    if (data.status === "ok") {
-        window.location.href = "/hosts";
-    } else {
-        document.getElementById("loginError").textContent = data.error;
+    // Riferimenti UI
+    const form = document.getElementById("loginForm");
+    const btn = form.querySelector(".btn-login");
+    const userEl = document.getElementById("username");
+    const passEl = document.getElementById("password");
+    const errorBox = document.getElementById("loginError");
+
+    // Pulizia stato precedente
+    errorBox.classList.add("d-none");
+    errorBox.textContent = "";
+    userEl.removeAttribute("aria-invalid");
+    passEl.removeAttribute("aria-invalid");
+
+    // Normalizza input
+    const user = userEl.value.trim();
+    const pass = passEl.value;
+
+    // Evita submit multipli
+    if (btn.disabled) return;
+
+    // Disabilita UI + spinner
+    const originalBtnHTML = btn.innerHTML;
+    btn.disabled = true;
+    btn.setAttribute("aria-busy", "true");
+    btn.innerHTML = `
+        <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
+        Accesso...
+    `;
+
+    try {
+        const res = await fetch("/api/login", {
+            method: "POST",
+            headers: { "Content-Type": "application/json" },
+            credentials: "include",
+            body: JSON.stringify({ username: user, password: pass })
+        });
+
+        // Prova a leggere JSON in modo resiliente
+        let data = {};
+        try {
+            data = await res.json();
+        } catch {
+            // Se il server non risponde con JSON valido
+            data = { status: "error", error: "Risposta non valida dal server." };
+        }
+
+        // Gestione HTTP non-OK come errore
+        if (!res.ok) {
+            const errMsg = data?.error || `Errore ${res.status} durante il login.`;
+            throw new Error(errMsg);
+        }
+
+        // Gestione logica dell'API
+        if (data.status === "ok") {
+            // Redirect alla pagina host
+            window.location.href = "/hosts";
+            return;
+        } else {
+            const msg = data.error || "Credenziali non valide.";
+            showError(msg, { highlight: true });
+            return;
+        }
+    } catch (err) {
+        // Errori di rete o eccezioni
+        const msg = err?.message || "Errore di connessione. Riprova.";
+        showError(msg, { highlight: true });
+    } finally {
+        // Ripristina il bottone
+        btn.disabled = false;
+        btn.removeAttribute("aria-busy");
+        btn.innerHTML = originalBtnHTML;
+    }
+
+    // ---- helpers locali ----
+    function showError(message, opts = {}) {
+        errorBox.textContent = message;
+        errorBox.classList.remove("d-none");
+        // evidenziazione campi e focus
+        if (opts.highlight) {
+            // metti invalid sui campi solo se sono vuoti o se credenziali errate
+            if (!user) userEl.setAttribute("aria-invalid", "true");
+            if (!pass) passEl.setAttribute("aria-invalid", "true");
+            // in caso di credenziali errate, metti focus allo username
+            userEl.focus();
+            // opzionale: aggiungi classe is-invalid se vuoi la resa Bootstrap
+            // userEl.classList.add("is-invalid");
+            // passEl.classList.add("is-invalid");
+        }
     }
 }
+
+// (Opzionale) Reset errore on input: nasconde alert quando l’utente modifica i campi
+document.addEventListener("DOMContentLoaded", () => {
+    const errorBox = document.getElementById("loginError");
+    const userEl = document.getElementById("username");
+    const passEl = document.getElementById("password");
+
+    function clearError() {
+        if (!errorBox.classList.contains("d-none")) {
+            errorBox.classList.add("d-none");
+            errorBox.textContent = "";
+        }
+        userEl.removeAttribute("aria-invalid");
+        passEl.removeAttribute("aria-invalid");
+        // userEl.classList.remove("is-invalid");
+        // passEl.classList.remove("is-invalid");
+    }
+
+    userEl.addEventListener("input", clearError);
+    passEl.addEventListener("input", clearError);
+});
index 929ee50d4ed27530ddf2556b265f6d16b93b1fe5..af7f8ba895f78fdce0645b859f3f344722652050 100644 (file)
@@ -4,43 +4,83 @@
     <meta charset="UTF-8">
     <title>Network Manager - Login</title>
     <meta name="viewport" content="width=device-width, initial-scale=1">
+
+    <!-- Bootstrap 5.x CSS (CDN) -->
+    <link
+        href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
+        rel="stylesheet"
+        integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
+        crossorigin="anonymous"
+    >
+    <!-- Bootstrap Icons -->
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
+
+    <!-- Boostrap override -->
     <link rel="stylesheet" href="css/variables.css">
     <link rel="stylesheet" href="css/layout.css">
-    <link rel="stylesheet" href="css/login.css">
 </head>
-<body class="body-login">
 
-<div class="login-wrapper">
-    <div class="login-box">
-        <div class="login-box-header">
-            <svg width="34" height="34" viewBox="0 0 24 24" fill="var(--accent)" aria-hidden="true">
-                <circle cx="12" cy="4" r="2"/>
-                <circle cx="4" cy="12" r="2"/>
-                <circle cx="20" cy="12" r="2"/>
-                <circle cx="12" cy="20" r="2"/>
-                <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"/>
-                <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"/>
-            </svg>
-            <span>Network Manager</span>
-        </div>
+<body class="body-login">
+    <!-- Main wrapper -->
+    <div class="login-wrapper">
+        <div class="login-box">
+            <!-- banner with logo -->
+            <div class="login-box-header">
+                <svg width="34" height="34" viewBox="0 0 24 24" fill="var(--accent)" aria-hidden="true">
+                    <circle cx="12" cy="4" r="2"></circle>
+                    <circle cx="4" cy="12" r="2"></circle>
+                    <circle cx="20" cy="12" r="2"></circle>
+                    <circle cx="12" cy="20" r="2"></circle>
+                    <line x1="12" y1="6" x2="12" y2="18" stroke="var(--accent)" stroke-width="2"></line>
+                    <line x1="6" y1="12" x2="18" y2="12" stroke="var(--accent)" stroke-width="2"></line>
+                </svg>
+                <span>Network Manager</span>
+            </div>
 
-        <h2>Login</h2>
+            <h2 class="mb-3">Login</h2>
 
-        <form id="loginForm" onsubmit="return handleLogin(event)">
-            <label for="username">Username</label>
-            <input type="text" id="username" autocomplete="off" required>
+            <form id="loginForm" onsubmit="return handleLogin(event)">
+                <div class="mb-3">
+                    <!--<label for="username" class="form-label">Username</label>-->
+                    <input
+                        type="text"
+                        id="username"
+                        class="form-control"
+                        autocomplete="username"
+                        placeholder="username"
+                        required
+                        aria-describedby="usernameHelp"
+                    >
+                </div>
 
-            <label for="password">Password</label>
-            <input type="password" id="password" required>
+                <div class="mb-3">
+                    <!--<label for="password" class="form-label">Password</label>-->
+                    <input
+                        type="password"
+                        id="password"
+                        class="form-control"
+                        autocomplete="current-password"
+                        placeholder="password"
+                        required
+                    >
+                </div>
 
-            <button type="submit" class="btn-primary login-btn">Entra</button>
-        </form>
+                <button type="submit" class="btn btn-primary btn-login">
+                    Entra
+                </button>
+            </form>
 
-        <div id="loginError" class="login-error"></div>
+            <!-- Error message -->
+            <div id="loginError" class="alert alert-danger mt-3 d-none" role="alert"></div>
+        </div>
     </div>
-</div>
 
-<script src="js/login.js"></script>
+       <!-- Scripts -->
+    <script src="js/login.js"></script>
 
+    <!-- Bootstrap JS -->
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
+            integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
+            crossorigin="anonymous"></script>
 </body>
-</html>
\ No newline at end of file
+</html>