From 22b429f2479df2f86f861a24dcba49d98cda8bb5 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Fri, 22 May 2026 22:53:05 +0200 Subject: [PATCH] feat: dashboard dynamique + RAM min/max dans popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Grid: nouvel agent ajouté en temps réel dès le 1er paquet WebSocket (plus besoin d'actualiser la page) - Grid: ip/status mis à jour depuis chaque metrics_update (adresse DHCP fraîche) - WS: diffuse agent_removed lors de la suppression d'un agent (sync multi-onglets) - Popup détail: min/max RAM sur la période affichée (calculé depuis l'historique déjà chargé) - CSS: classe .chart-minmax pour l'affichage min/max sous le graphe Co-Authored-By: Claude Sonnet 4.6 --- dashboard/css/app.css | 1 + dashboard/js/app.js | 2 ++ dashboard/js/grid.js | 20 ++++++++++++++++++-- dashboard/js/popups.js | 12 ++++++++++++ server/handlers/agents.go | 4 +++- server/main.go | 2 +- 6 files changed, 37 insertions(+), 4 deletions(-) diff --git a/dashboard/css/app.css b/dashboard/css/app.css index 135772f..594949a 100644 --- a/dashboard/css/app.css +++ b/dashboard/css/app.css @@ -187,6 +187,7 @@ body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-s .chart-cur{font-family:var(--font-mono);font-size:16px;font-weight:700} .chart-svg{width:100%;height:52px;display:block} .chart-axis{display:flex;justify-content:space-between;margin-top:2px;font-family:var(--font-terminal);font-size:9px;color:var(--ink-4)} +.chart-minmax{display:flex;justify-content:space-between;margin-top:3px;font-family:var(--font-mono);font-size:9px;color:var(--ink-4)} .smart-btn{display:inline-flex;align-items:center;gap:8px;padding:7px 12px;border-radius:8px; border:1px solid var(--border-2);background:var(--bg-3);cursor:pointer; transition:background .12s,border-color .12s,transform .08s;font-family:var(--font-terminal);font-size:11px} diff --git a/dashboard/js/app.js b/dashboard/js/app.js index 016e650..1bb79a3 100644 --- a/dashboard/js/app.js +++ b/dashboard/js/app.js @@ -87,6 +87,8 @@ const App = (() => { updateServerStats(msg.data); } else if (msg.type === 'status_update') { Grid.updateStatus(msg.agent_id, msg.data.status); + } else if (msg.type === 'agent_removed') { + Grid.removeAgent(msg.agent_id); } } catch {} }; diff --git a/dashboard/js/grid.js b/dashboard/js/grid.js index 7ccb64b..1bd187c 100644 --- a/dashboard/js/grid.js +++ b/dashboard/js/grid.js @@ -111,8 +111,24 @@ const Grid = (() => { } function update(agentId, metrics) { - const entry = _agents.get(agentId); - if (!entry) return; + let entry = _agents.get(agentId); + if (!entry) { + // Nouvel agent découvert via WebSocket — on crée la tuile à la volée + const agent = { + id: agentId, + hostname: metrics.hostname || agentId, + ip: metrics.ip || '', + status: metrics.status || 'online', + }; + _agents.set(agentId, { agent, metrics }); + const grid = document.getElementById('agents-grid'); + if (grid) grid.insertAdjacentHTML('beforeend', renderTile(agent, metrics)); + updateStats(); + return; + } + // Mettre à jour ip/status depuis les métriques fraîches + if (metrics.ip) entry.agent.ip = metrics.ip; + if (metrics.status) entry.agent.status = metrics.status; // Conserver les valeurs lentes (disque, smart) quand le paquet ne les contient pas if (entry.metrics) { for (const k of Object.keys(entry.metrics)) { diff --git a/dashboard/js/popups.js b/dashboard/js/popups.js index 51d86f8..b95a21c 100644 --- a/dashboard/js/popups.js +++ b/dashboard/js/popups.js @@ -69,6 +69,17 @@ const Popups = (() => { const cpuPts = Charts.historyToCpuPts(history); const memPts = Charts.historyToMemPts(history); + let ramMin = null, ramMax = null; + for (const h of history) { + if (h.memory_used != null) { + if (ramMin === null || h.memory_used < ramMin) ramMin = h.memory_used; + if (ramMax === null || h.memory_used > ramMax) ramMax = h.memory_used; + } + } + const ramMinMax = ramMin !== null + ? `
min ${Grid.fmt(ramMin)}max ${Grid.fmt(ramMax)}
` + : ''; + const smartBtn = metrics?.smart ? `
@@ -118,6 +129,7 @@ const Popups = (() => {
−30min−15minnow
+ ${ramMinMax} diff --git a/server/handlers/agents.go b/server/handlers/agents.go index 1708320..6566414 100644 --- a/server/handlers/agents.go +++ b/server/handlers/agents.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/user/nanometrics/server/db" + "github.com/user/nanometrics/server/models" ) func AgentsHandler(database *db.DB) http.HandlerFunc { @@ -23,7 +24,7 @@ func AgentsHandler(database *db.DB) http.HandlerFunc { } } -func DeleteAgentHandler(database *db.DB) http.HandlerFunc { +func DeleteAgentHandler(database *db.DB, broadcast func(interface{})) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") if len(parts) < 3 { @@ -35,6 +36,7 @@ func DeleteAgentHandler(database *db.DB) http.HandlerFunc { http.Error(w, err.Error(), 500) return } + broadcast(models.WSMessage{Type: "agent_removed", AgentID: agentID}) w.WriteHeader(http.StatusNoContent) } } diff --git a/server/main.go b/server/main.go index 161ac2d..b0f9acc 100644 --- a/server/main.go +++ b/server/main.go @@ -111,7 +111,7 @@ func main() { case endsWith(r.URL.Path, "/icon") && r.Method == http.MethodGet: handlers.IconGetHandler(database)(w, r) case r.Method == http.MethodDelete: - handlers.DeleteAgentHandler(database)(w, r) + handlers.DeleteAgentHandler(database, hub.Broadcast)(w, r) default: http.NotFound(w, r) }