From 13c164e7d711649cf36c1c65db839a4d91371e75 Mon Sep 17 00:00:00 2001 From: Giorgio Ravera Date: Thu, 19 Mar 2026 22:30:15 +0100 Subject: [PATCH] Added index.html --- frontend/css/layout.css | 43 +++++++ frontend/index.html | 252 ++++++++++++++++++++++++++++++++++++++++ frontend/js/index.js | 132 ++++++++++++++++++++- frontend/js/services.js | 55 +++++++++ 4 files changed, 480 insertions(+), 2 deletions(-) create mode 100644 frontend/index.html diff --git a/frontend/css/layout.css b/frontend/css/layout.css index a5d730d..82a079a 100644 --- a/frontend/css/layout.css +++ b/frontend/css/layout.css @@ -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 index 0000000..f3e0022 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,252 @@ + + + + + Network Manager + + + + + + + + + + + + + + +
+
+ + + +
+ + +
+
+ + +
+ + +
+
+
+ +
+

+ 🖧 + Dashboard +

+
+
+
+
+ + + + +
+ +
+ + +
+
+

DNS (BIND)

+

Zone, record e configurazioni.

+
+ +
+
+ + +
+
+

DHCP (Kea)

+

Pools, leases, reservations.

+
+ +
+
+ + + +
+

Hosts

+

Inventario IP/MAC.

+
+ + + +
+

Aliases

+

Inventario alias DNS.

+
+ + + +
+

Certificati

+

Let’s Encrypt e rinnovi.

+
+ + +
+
+

Backup & Restore

+

Esecuzione backup e gestione archivi.

+ +
+ + +
+
+ + + +
+

Logs

+

Eventi e access log.

+
+ + + +
+

Impostazioni

+

Configurazione sistema e variabili.

+
+ + + +
+

Health

+

Stato servizi e risorse.

+
+ +
+
+ + + + + + + + + + + + + + + diff --git a/frontend/js/index.js b/frontend/js/index.js index a7331a8..5ffebca 100644 --- a/frontend/js/index.js +++ b/frontend/js/index.js @@ -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 = ` + ${r.label} + ${r.value} + `; + 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'); diff --git a/frontend/js/services.js b/frontend/js/services.js index 492c848..0055701 100644 --- a/frontend/js/services.js +++ b/frontend/js/services.js @@ -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 ?? []); +} -- 2.47.3