]> git.giorgioravera.it Git - network-manager.git/commitdiff
Added index.html
authorGiorgio Ravera <giorgio.ravera@gmail.com>
Thu, 19 Mar 2026 21:30:15 +0000 (22:30 +0100)
committerGiorgio Ravera <giorgio.ravera@gmail.com>
Thu, 19 Mar 2026 21:30:15 +0000 (22:30 +0100)
frontend/css/layout.css
frontend/index.html [new file with mode: 0644]
frontend/js/index.js
frontend/js/services.js

index a5d730d616d00ee6bf226c593b2471192e5b998b..82a079a12b3f4a86a1f8d6c4f59c95f1a044f261 100644 (file)
@@ -136,6 +136,49 @@ body {
     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
    ================================ */
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644 (file)
index 0000000..f3e0022
--- /dev/null
@@ -0,0 +1,252 @@
+<!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 &amp; 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>
index a7331a8cff1f97f222df6cd0ca0a3aee3e0644f2..5ffebcae04ab4637f7f00142c994edb210c6894f 100644 (file)
@@ -2,7 +2,7 @@
 // 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
@@ -24,6 +24,123 @@ function closeRestoreModal() {
     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
 // -------------------------------------------------------
@@ -124,6 +241,14 @@ const actionHandlers = {
         } else {
             showToast('Error updating API status', false);
         }
+    },
+    // Health
+    openHealthModal: (e) => {
+        if (e?.preventDefault) e.preventDefault();
+        openHealthModal();
+    },
+    closeHealthModal: () => {
+        closeHealthModal();
     }
 };
 
@@ -150,7 +275,10 @@ document.addEventListener('click', async (e) => {
 // MODAL: ESC + BACKDROP CLOSE
 // -------------------------------------------------------
 document.addEventListener('keydown', (e) => {
-    if (e.key === 'Escape') closeRestoreModal();
+    if (e.key === 'Escape') {
+        closeRestoreModal();
+        closeHealthModal();
+    }
 });
 
 const restoreModal = document.getElementById('restoreModal');
index 492c8482542f996ada185e12867a9abcfaa736ae..00557013a5a6429257ddb07afaaf53e0cff21645 100644 (file)
@@ -262,3 +262,58 @@ export async function doRestore(id) {
         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 ?? []);
+}