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():
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():
+++ /dev/null
-/* ================================
- 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: "▼";
-}
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);
}
/* ================================
.topbar {
width: 100%;
background: var(--bg-dark);
- padding: 0;
- display: flex;
- align-items: center;
border-bottom: 3px solid var(--accent);
position: fixed;
top: 0;
.topbar-inner {
width: 100%;
margin: 0 auto;
- padding: 14px 26px;
+ padding: 10px 26px;
display: flex;
justify-content: space-between;
align-items: center;
}
/* ================================
- 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;
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;
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;
+ }
}
+++ /dev/null
-/* ================================
- 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
--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);
}
<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>
+// -----------------------------
+// Configuration parameters
+// -----------------------------
+let timeoutToast = 3000; // milliseconds
+
+// -----------------------------
+// State variables
+// -----------------------------
let editingHostId = null;
let sortDirection = {};
let lastSort = null; // { colIndex: number, ascending: boolean }
// 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);
// 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 = "✔";
- 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);
}
// -----------------------------
-// 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) {
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;
}
// -----------------------------
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
// -----------------------------
setTimeout(() => {
toast.classList.remove("show");
- }, 2500);
+ }, timeoutToast);
}
// -----------------------------
}
// -----------------------------
-// INITIAL TABLE LOAD
+// DOMContentLoaded: initialize everything
// -----------------------------
document.addEventListener("DOMContentLoaded", async () => {
// 1) Init UI sort (aria-sort, arrows)
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
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") {
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();
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);
+ }
+ });
+ }
+});
+
// -----------------------------
-// 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);
+});
<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>