feat: dashboard dynamique + RAM min/max dans popup
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a2060a1713
commit
22b429f247
@@ -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-cur{font-family:var(--font-mono);font-size:16px;font-weight:700}
|
||||||
.chart-svg{width:100%;height:52px;display:block}
|
.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-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;
|
.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;
|
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}
|
transition:background .12s,border-color .12s,transform .08s;font-family:var(--font-terminal);font-size:11px}
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ const App = (() => {
|
|||||||
updateServerStats(msg.data);
|
updateServerStats(msg.data);
|
||||||
} else if (msg.type === 'status_update') {
|
} else if (msg.type === 'status_update') {
|
||||||
Grid.updateStatus(msg.agent_id, msg.data.status);
|
Grid.updateStatus(msg.agent_id, msg.data.status);
|
||||||
|
} else if (msg.type === 'agent_removed') {
|
||||||
|
Grid.removeAgent(msg.agent_id);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-2
@@ -111,8 +111,24 @@ const Grid = (() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update(agentId, metrics) {
|
function update(agentId, metrics) {
|
||||||
const entry = _agents.get(agentId);
|
let entry = _agents.get(agentId);
|
||||||
if (!entry) return;
|
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
|
// Conserver les valeurs lentes (disque, smart) quand le paquet ne les contient pas
|
||||||
if (entry.metrics) {
|
if (entry.metrics) {
|
||||||
for (const k of Object.keys(entry.metrics)) {
|
for (const k of Object.keys(entry.metrics)) {
|
||||||
|
|||||||
@@ -69,6 +69,17 @@ const Popups = (() => {
|
|||||||
const cpuPts = Charts.historyToCpuPts(history);
|
const cpuPts = Charts.historyToCpuPts(history);
|
||||||
const memPts = Charts.historyToMemPts(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
|
||||||
|
? `<div class="chart-minmax"><span>min ${Grid.fmt(ramMin)}</span><span>max ${Grid.fmt(ramMax)}</span></div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
const smartBtn = metrics?.smart
|
const smartBtn = metrics?.smart
|
||||||
? `<div class="smart-btn ok" onclick="Popups.showSmart('${esc(agentId)}')" data-tip="Voir la santé complète du disque">
|
? `<div class="smart-btn ok" onclick="Popups.showSmart('${esc(agentId)}')" data-tip="Voir la santé complète du disque">
|
||||||
<div class="smart-dot"></div>
|
<div class="smart-dot"></div>
|
||||||
@@ -118,6 +129,7 @@ const Popups = (() => {
|
|||||||
</div>
|
</div>
|
||||||
<svg class="chart-svg" viewBox="0 0 200 52" preserveAspectRatio="none" id="det-mem-chart"></svg>
|
<svg class="chart-svg" viewBox="0 0 200 52" preserveAspectRatio="none" id="det-mem-chart"></svg>
|
||||||
<div class="chart-axis"><span>−30min</span><span>−15min</span><span>now</span></div>
|
<div class="chart-axis"><span>−30min</span><span>−15min</span><span>now</span></div>
|
||||||
|
${ramMinMax}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/user/nanometrics/server/db"
|
"github.com/user/nanometrics/server/db"
|
||||||
|
"github.com/user/nanometrics/server/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AgentsHandler(database *db.DB) http.HandlerFunc {
|
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) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
@@ -35,6 +36,7 @@ func DeleteAgentHandler(database *db.DB) http.HandlerFunc {
|
|||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
broadcast(models.WSMessage{Type: "agent_removed", AgentID: agentID})
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -111,7 +111,7 @@ func main() {
|
|||||||
case endsWith(r.URL.Path, "/icon") && r.Method == http.MethodGet:
|
case endsWith(r.URL.Path, "/icon") && r.Method == http.MethodGet:
|
||||||
handlers.IconGetHandler(database)(w, r)
|
handlers.IconGetHandler(database)(w, r)
|
||||||
case r.Method == http.MethodDelete:
|
case r.Method == http.MethodDelete:
|
||||||
handlers.DeleteAgentHandler(database)(w, r)
|
handlers.DeleteAgentHandler(database, hub.Broadcast)(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user