font-style: italic;
}
+/* Griglia tile */
+.tiles {
+ display: grid;
+ gap: 16px;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+}
+
+/* Tile stile pfSense-like come login-box */
+.tile {
+ background: #fff;
+ border: 1px solid var(--border-light);
+ border-left: 4px solid var(--accent);
+ border-radius: 8px;
+ padding: 14px;
+ box-shadow: 0 3px 6px var(--shadow-soft);
+ transition: transform 0.1s ease, box-shadow 0.1s ease;
+ color: var(--text-dark);
+}
+
+.tile:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 10px var(--shadow-strong);
+ border-color: var(--accent-hover);
+}
+
+.tile h3 {
+ margin: 8px 0 4px 0;
+ font-size: 1.05rem;
+}
+
+.tile p {
+ margin: 0;
+ color: #333;
+ font-size: 0.9rem;
+}
+
+/* Icone tile */
+.tile-icon {
+ font-size: 28px;
+ color: var(--accent);
+ margin-bottom: 4px;
+}
+
/* ================================
Buttons
================================ */
--- /dev/null
+<!DOCTYPE html>
+<html>
+<head>
+ <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">
+</head>
+
+<body>
+ <!-- Topbar -->
+ <header class="topbar">
+ <div class="topbar-inner">
+ <a href="/home" class="logo text-decoration-none">
+ <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>
+ </a>
+
+ <!-- Spacer -->
+ <div class="col d-none d-md-block"></div>
+
+ <nav class="navbar navbar-expand-md px-3">
+ <!-- Bottone hamburger -->
+ <button class="navbar-toggler bg-light" type="button" data-bs-toggle="collapse" data-bs-target="#menuNav">
+ <span class="navbar-toggler-icon"></span>
+ </button>
+ <!-- Menu -->
+ <div class="collapse navbar-collapse" id="menuNav">
+ <div class="navbar-nav ms-auto gap-2">
+ <button id="api-pill" class="btn btn-outline-primary" title="API Status" aria-label="API Status" data-action="apiCheck">API Status</button>
+ <button id="logoutBtn" class="btn btn-primary">Logout</button>
+ </div>
+ </div>
+ </nav>
+ </div>
+ </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>
+ <span class="section-title">Dashboard</span>
+ </h2>
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <!-- ============================ -->
+ <!-- CONTENUTO PRINCIPALE -->
+ <!-- ============================ -->
+ <main class="container">
+
+ <div class="tiles">
+
+ <!-- DNS -->
+ <div class="tile">
+ <div class="tile-icon"><i class="bi bi-diagram-3"></i></div>
+ <h3>DNS (BIND)</h3>
+ <p>Zone, record e configurazioni.</p>
+ <div class="mt-2 d-flex gap-2 flex-wrap">
+ <button class="btn btn-primary btn-sm" title="Reload DNS (BIND)" aria-label="Reload DNS" data-action="reloadDns">
+ <i class="bi bi-arrow-repeat"></i><span class="label"> Reload DNS</span>
+ </button>
+ </div>
+ </div>
+
+ <!-- DHCP -->
+ <div class="tile">
+ <div class="tile-icon"><i class="bi bi-wifi"></i></div>
+ <h3>DHCP (Kea)</h3>
+ <p>Pools, leases, reservations.</p>
+ <div class="mt-2 d-flex gap-2 flex-wrap">
+ <button class="btn btn-primary btn-sm" title="Reload DHCP (Kea)" aria-label="Reload DHCP" data-action="reloadDhcp">
+ <i class="bi bi-arrow-repeat"></i><span class="label"> Reload DHCP</span>
+ </button>
+ </div>
+ </div>
+
+ <!-- Hosts -->
+ <a href="/hosts" class="tile text-decoration-none">
+ <div class="tile-icon"><i class="bi bi-hdd-network"></i></div>
+ <h3>Hosts</h3>
+ <p>Inventario IP/MAC.</p>
+ </a>
+
+ <!-- Aliases -->
+ <a href="/aliases" class="tile text-decoration-none">
+ <div class="tile-icon"><i class="bi bi-hdd-network"></i></div>
+ <h3>Aliases</h3>
+ <p>Inventario alias DNS.</p>
+ </a>
+
+ <!-- Certificati -->
+ <a href="/api/certificates" class="tile text-decoration-none">
+ <div class="tile-icon"><i class="bi bi-shield-lock"></i></div>
+ <h3>Certificati</h3>
+ <p>Let’s Encrypt e rinnovi.</p>
+ </a>
+
+ <!-- Backup & Restore -->
+ <div class="tile">
+ <div class="tile-icon"><i class="bi bi-save"></i></div>
+ <h3>Backup & Restore</h3>
+ <p>Esecuzione backup e gestione archivi.</p>
+
+ <div class="mt-2 d-flex gap-2 flex-wrap">
+ <button class="btn btn-primary btn-sm" title="Start Backup" aria-label="Start Backup" data-action="startBackup">
+ <i class="bi bi-cloud-upload"></i><span class="label"> Start Backup</span>
+ </button>
+ <button class="btn btn-primary btn-sm" title="Open Restore" aria-label="Open Restore" data-action="openRestoreModal">
+ <i class="bi bi-cloud-download"></i><span class="label"> Open Restore</span>
+ </button>
+ </div>
+ </div>
+
+ <!-- Logs -->
+ <a href="logs/index.html" class="tile text-decoration-none">
+ <div class="tile-icon"><i class="bi bi-file-earmark-text"></i></div>
+ <h3>Logs</h3>
+ <p>Eventi e access log.</p>
+ </a>
+
+ <!-- Settings -->
+ <a href="settings/index.html" class="tile text-decoration-none">
+ <div class="tile-icon"><i class="bi bi-gear"></i></div>
+ <h3>Impostazioni</h3>
+ <p>Configurazione sistema e variabili.</p>
+ </a>
+
+ <!-- Health -->
+ <a href="/api/health" id="healthBtn" class="tile text-decoration-none" data-action="openHealthModal" aria-controls="healthModal" role="button">
+ <div class="tile-icon"><i class="bi bi-heart-pulse"></i></div>
+ <h3>Health</h3>
+ <p>Stato servizi e risorse.</p>
+ </a>
+
+ </div>
+ </main>
+
+ <!-- Modal Restore -->
+ <div id="restoreModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="restoreTitle">
+ <div class="modal-content addhost-modal">
+ <div class="addhost-header d-flex justify-content-between align-items-center">
+ <div class="d-flex align-items-center gap-2">
+ <span class="title-icon">💾</span>
+ <span id="restoreTitle" class="modal-title">Execute Restore</span>
+ </div>
+ <button type="button" class="btn-close" title="Close" aria-label="Close" data-action="closeRestoreModal"></button>
+ </div>
+
+ <div class="modal-body">
+ <label for="restoreBackupId" class="form-label">Backup ID</label>
+ <input type="text" id="restoreBackupId" class="form-control" placeholder="e.g., bk_20260317_200100" />
+ <small class="text-muted">Select the backup ID to restore.</small>
+ </div>
+
+ <div class="modal-footer modal-buttons">
+ <button type="button" class="btn btn-primary" data-action="startRestore"><span class="label">Start Restore</span></button>
+ <button type="button" class="btn btn-outline-primary" data-action="closeRestoreModal"><span class="label">Cancel</span></button>
+ </div>
+ </div>
+ </div>
+
+ <!-- Modal Health -->
+ <div id="healthModal" class="modal" role="dialog" aria-modal="true" aria-labelledby="healthTitle">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content addhost-modal">
+ <div class="addhost-header d-flex justify-content-between align-items-center">
+ <div class="d-flex align-items-center gap-2">
+ <span class="title-icon">❤️🩹</span>
+ <span id="healthTitle" class="modal-title">Health Status</span>
+ </div>
+ <button type="button" class="btn-close" title="Close" aria-label="Close" data-action="closeHealthModal"></button>
+ </div>
+
+ <div class="modal-body">
+ <!-- Loading -->
+ <div id="healthLoading" class="d-flex align-items-center gap-2">
+ <div class="spinner-border text-primary spinner-border-sm" role="status" aria-label="Loading"></div>
+ <span class="text-muted">Loading status…</span>
+ </div>
+
+ <!-- Content -->
+ <div id="healthContent" class="d-none">
+ <ul class="list-group mb-3" id="healthSummary">
+ <!-- Popolato via JS -->
+ </ul>
+
+ <div class="mb-3">
+ <span class="badge rounded-pill" id="healthStatusBadge">—</span>
+ <small class="text-muted ms-2" id="healthUpdatedAt"></small>
+ </div>
+
+ <!--<details>
+ <summary class="mb-2">JSON completo</summary>
+ <pre class="bg-light p-2 rounded small mb-0" id="healthRawJson" aria-label="Raw JSON"></pre>
+ </details>-->
+ </div>
+
+ <!-- Error -->
+ <div id="healthError" class="alert alert-danger d-none" role="alert" aria-live="assertive">
+ Error fetching health status. Please try again later.
+ </div>
+ </div>
+
+ <div class="modal-footer modal-buttons">
+ <button type="button" class="btn btn-outline-primary" data-action="closeHealthModal">
+ <span class="label">Close</span>
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Scripts -->
+ <script type=module src="js/index.js"></script>
+ <script type="module" src="js/session.js"></script>
+
+ <!-- Bootstrap JS -->
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
+ integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
+ crossorigin="anonymous"></script>
+</body>
+</html>
// IMPORT
// -------------------------------------------------------
import { showToast } from './common.js';
-import { apiCheck, reloadDNS, reloadDHCP, doBackup, doRestore } from './services.js';
+import { apiCheck, reloadDNS, reloadDHCP, doBackup, doRestore, checkHealth } from './services.js';
// -------------------------------------------------------
// RESTORE MODAL OPEN/CLOSE
if (modal) modal.style.display = 'none';
}
+// -------------------------------------------------------
+// HEALTH MODAL OPEN/CLOSE + RENDER (usa checkHealth())
+// -------------------------------------------------------
+function openHealthModal() {
+ const modal = document.getElementById('healthModal');
+ const loadingEl = document.getElementById('healthLoading');
+ const contentEl = document.getElementById('healthContent');
+ const errorEl = document.getElementById('healthError');
+ const badgeEl = document.getElementById('healthStatusBadge');
+ const updatedAtEl = document.getElementById('healthUpdatedAt');
+ const summaryEl = document.getElementById('healthSummary');
+ //const rawJsonEl = document.getElementById('healthRawJson');
+
+ if (!modal) return;
+
+ // Reset UI
+ modal.style.display = 'flex';
+ loadingEl?.classList?.remove('d-none');
+ contentEl?.classList?.add('d-none');
+ errorEl?.classList?.add('d-none');
+ if (summaryEl) summaryEl.innerHTML = '';
+ //if (rawJsonEl) rawJsonEl.textContent = '';
+ if (badgeEl) {
+ badgeEl.className = 'badge rounded-pill bg-secondary';
+ badgeEl.textContent = '—';
+ }
+ if (updatedAtEl) updatedAtEl.textContent = '';
+
+ // Usa checkHealth() per ottenere il payload health
+ Promise.resolve()
+ .then(() => checkHealth())
+ .then((data) => {
+ // Se checkHealth ritorna true o {message}, non abbiamo i dettagli: mostra un messaggio
+ const isDetailed =
+ data && typeof data === 'object' &&
+ ('status' in data || 'latency_ms' in data || 'database' in data);
+
+ if (!isDetailed) {
+ throw new Error('Health details not available');
+ }
+
+ renderHealth(data);
+ loadingEl?.classList?.add('d-none');
+ contentEl?.classList?.remove('d-none');
+ })
+ .catch((err) => {
+ loadingEl?.classList?.add('d-none');
+ errorEl?.classList?.remove('d-none');
+ showToast(err?.message || 'Error while fetching health status', false);
+ console.error(err);
+ });
+}
+
+function closeHealthModal() {
+ const modal = document.getElementById('healthModal');
+ if (modal) modal.style.display = 'none';
+}
+
+function setHealthBadge(status) {
+ const badgeEl = document.getElementById('healthStatusBadge');
+ if (!badgeEl) return;
+
+ const norm = String(status || '').toLowerCase();
+ let cls = 'bg-secondary';
+ if (norm === 'ok' || norm === 'healthy' || norm === 'up') cls = 'bg-success';
+ if (norm === 'warn' || norm === 'warning' || norm === 'degraded') cls = 'bg-warning text-dark';
+ if (norm === 'down' || norm === 'error' || norm === 'fail' || norm === 'critical') cls = 'bg-danger';
+
+ badgeEl.className = `badge rounded-pill ${cls}`;
+ badgeEl.textContent = norm || 'unknown';
+}
+
+function renderHealth(data) {
+ const summaryEl = document.getElementById('healthSummary');
+ //const rawJsonEl = document.getElementById('healthRawJson');
+ const updatedAtEl = document.getElementById('healthUpdatedAt');
+
+ const status = data?.status ?? 'unknown';
+ const latency = data?.latency_ms;
+ const db = data?.database ?? {};
+ const dbStatus = db?.status ?? 'unknown';
+ const dbVersion = db?.version ?? '—';
+ const dbTables = (typeof db?.tables === 'number') ? db.tables : '—';
+ const dbSize = (typeof db?.size_mb === 'number') ? `${db.size_mb} MB` : '—';
+
+ setHealthBadge(status);
+ if (updatedAtEl) {
+ const now = new Date();
+ updatedAtEl.textContent = `Updated at ${now.toLocaleTimeString()}`;
+ }
+
+ const rows = [
+ { label: 'Status', value: status },
+ { label: 'Latency', value: (typeof latency === 'number') ? `${latency} ms` : '—' },
+ { label: 'DB Status', value: dbStatus },
+ { label: 'DB Version', value: dbVersion },
+ { label: 'DB Tables', value: dbTables },
+ { label: 'DB Size', value: dbSize },
+ ];
+
+ if (summaryEl) {
+ for (const r of rows) {
+ const li = document.createElement('li');
+ li.className = 'list-group-item d-flex justify-content-between align-items-center';
+ li.innerHTML = `
+ <span class="text-muted">${r.label}</span>
+ <strong>${r.value}</strong>
+ `;
+ summaryEl.appendChild(li);
+ }
+ }
+
+ //if (rawJsonEl) {
+ // rawJsonEl.textContent = JSON.stringify(data, null, 2);
+ //}
+}
+
// -------------------------------------------------------
// Action Handlers
// -------------------------------------------------------
} else {
showToast('Error updating API status', false);
}
+ },
+ // Health
+ openHealthModal: (e) => {
+ if (e?.preventDefault) e.preventDefault();
+ openHealthModal();
+ },
+ closeHealthModal: () => {
+ closeHealthModal();
}
};
// MODAL: ESC + BACKDROP CLOSE
// -------------------------------------------------------
document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') closeRestoreModal();
+ if (e.key === 'Escape') {
+ closeRestoreModal();
+ closeHealthModal();
+ }
});
const restoreModal = document.getElementById('restoreModal');
return data?.message ? { message: data.message } : false;
}
}
+
+// -------------------------------------------------------
+// Check Health
+// -------------------------------------------------------
+export async function checkHealth() {
+ let res;
+
+ try {
+ // Fetch data
+ res = await fetch(`/api/health`, {
+ method: 'GET',
+ headers: { 'Accept': 'application/json' }
+ });
+
+ } catch (err) {
+ const msg = 'Network error while performing health check' + (err?.message ? `: ${err.message}` : '');
+ throw new Error(msg, { cause: err });
+ }
+
+ // Success without JSON
+ if (res.status === 204) {
+ return true;
+ }
+
+ // Check content-type to avoid parsing errors
+ const contentType = res.headers.get("content-type") || "";
+ if (!contentType.includes("application/json")) {
+ const err = new Error(`Error performing health check: ${res.status}: ${res.statusText || 'Unexpected response'}`);
+ err.status = res.status;
+ throw err;
+ }
+
+ // Check JSON
+ let data = {};
+ try {
+ data = await res.json();
+ } catch {
+ throw new Error('Error performing health check: Invalid JSON payload');
+ }
+
+ // Check JSON errors
+ if (!res.ok) {
+ const serverMsg =
+ data?.detail?.message?.trim()
+ || (typeof data?.detail === 'string' ? data.detail.trim() : '')
+ || data?.message?.trim()
+ || data?.error?.message?.trim()
+ || (typeof data?.error === 'string' ? data.error.trim() : '');
+ const err = new Error('Error performing health check' + (serverMsg ? `: ${serverMsg}` : ''));
+ err.status = res.status;
+ throw err;
+ }
+
+ return (data ?? []);
+}