diff --git a/dashboard/css/app.css b/dashboard/css/app.css index a6e0e95..5b46d9d 100644 --- a/dashboard/css/app.css +++ b/dashboard/css/app.css @@ -118,6 +118,10 @@ body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-s .g-val{font-family:var(--font-mono);font-size:11px;color:var(--ink-2);width:34px;text-align:right} .tile-foot{font-family:var(--font-terminal);font-size:10px;color:var(--ink-4); display:flex;align-items:center;gap:5px;user-select:none} +.btn-del-agent{margin-left:auto;border:none;background:transparent;cursor:pointer; + color:var(--ink-5);font-size:11px;padding:2px 4px;border-radius:4px; + line-height:1;transition:color .15s,background .15s;user-select:none} +.btn-del-agent:hover{color:var(--err);background:color-mix(in srgb,var(--err) 12%,transparent)} /* FOOTER */ .footer{background:var(--bg-0);border-top:1px solid var(--border-2);height:26px; diff --git a/dashboard/index.html b/dashboard/index.html index ba3c1a9..5496a3e 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -147,6 +147,29 @@ + + + diff --git a/dashboard/js/api.js b/dashboard/js/api.js index eb5c6d0..2963797 100644 --- a/dashboard/js/api.js +++ b/dashboard/js/api.js @@ -27,6 +27,11 @@ const API = (() => { if (!r.ok) throw new Error(`PUT ${path}: ${r.status}`); } + async function del(path) { + const r = await fetch(BASE + path, { method: 'DELETE' }); + if (!r.ok) throw new Error(`DELETE ${path}: ${r.status}`); + } + async function postForm(path, formData) { const r = await fetch(BASE + path, { method: 'POST', body: formData }); if (!r.ok) throw new Error(`POST ${path}: ${r.status}`); @@ -44,6 +49,7 @@ const API = (() => { fd.append('icon', file); return postForm(`/api/agents/${id}/icon`, fd); }, + deleteAgent: (id) => del(`/api/agents/${id}`), iconUrl: (id) => `/api/agents/${id}/icon`, }; })(); diff --git a/dashboard/js/grid.js b/dashboard/js/grid.js index baee878..23dff7f 100644 --- a/dashboard/js/grid.js +++ b/dashboard/js/grid.js @@ -81,7 +81,7 @@ const Grid = (() => {
- ${offline ? '—' : fmtPct(memPct)} + ${offline ? '—' : (metrics?.memory_used && metrics?.memory_total ? fmt(metrics.memory_used) + '/' + fmt(metrics.memory_total) : '—')}
@@ -94,6 +94,10 @@ const Grid = (() => { ${offline ? 'Hors ligne' : `${uptimeStr}`} +
`; } @@ -139,6 +143,13 @@ const Grid = (() => { document.getElementById('stat-err').textContent = err; } + function removeAgent(id) { + _agents.delete(id); + const el = document.getElementById('tile-' + id); + if (el) el.remove(); + updateStats(); + } + function getAgent(id) { return _agents.get(id); } function updateStatus(agentId, status) { @@ -150,5 +161,5 @@ const Grid = (() => { updateStats(); } - return { refresh, update, updateStatus, getAgent, fmt, fmtPct }; + return { refresh, update, updateStatus, removeAgent, getAgent, fmt, fmtPct }; })(); diff --git a/dashboard/js/popups.js b/dashboard/js/popups.js index 8e0e679..d1158b1 100644 --- a/dashboard/js/popups.js +++ b/dashboard/js/popups.js @@ -406,10 +406,32 @@ const Popups = (() => { document.getElementById('overlay-smart').style.display = 'flex'; } + // ══ SUPPRESSION AGENT ══ + let _delAgentId = null; + + function confirmDeleteAgent(id, hostname) { + _delAgentId = id; + document.getElementById('del-agent-name').textContent = hostname || id; + document.getElementById('overlay-del').style.display = 'flex'; + } + + async function doDeleteAgent() { + if (!_delAgentId) return; + const id = _delAgentId; + document.getElementById('overlay-del').style.display = 'none'; + _delAgentId = null; + try { + await API.deleteAgent(id); + Grid.removeAgent(id); + } catch (e) { + alert('Erreur lors de la suppression : ' + e.message); + } + } + return { showDetail, hideDetail, showAgentCfg, sendAgentConfig, toggleCbox, - showSrvCfg, hideSrvCfg, saveSrvCfg, + showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent, showSmart, }; })(); diff --git a/server/db/db.go b/server/db/db.go index 2252a05..b54ed0b 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -238,6 +238,20 @@ func (d *DB) MarkOffline(timeoutSec int64) error { return err } +func (d *DB) DeleteAgent(agentID string) error { + for _, q := range []string{ + `DELETE FROM metrics WHERE agent_id = ?`, + `DELETE FROM agent_configs WHERE agent_id = ?`, + `DELETE FROM agent_icons WHERE agent_id = ?`, + `DELETE FROM agents WHERE id = ?`, + } { + if _, err := d.conn.Exec(q, agentID); err != nil { + return err + } + } + return nil +} + // MarkOfflineAndGetIDs marque les agents inactifs et retourne leurs IDs. func (d *DB) MarkOfflineAndGetIDs(timeoutSec int64) ([]string, error) { cutoff := time.Now().Unix() - timeoutSec diff --git a/server/handlers/agents.go b/server/handlers/agents.go index 47ace01..d9a37a1 100644 --- a/server/handlers/agents.go +++ b/server/handlers/agents.go @@ -3,6 +3,7 @@ package handlers import ( "encoding/json" "net/http" + "strings" "github.com/user/nanometrics/server/db" ) @@ -18,3 +19,19 @@ func AgentsHandler(database *db.DB) http.HandlerFunc { json.NewEncoder(w).Encode(agents) } } + +func DeleteAgentHandler(database *db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/") + if len(parts) < 3 { + http.Error(w, "invalid path", 400) + return + } + agentID := parts[2] + if err := database.DeleteAgent(agentID); err != nil { + http.Error(w, err.Error(), 500) + return + } + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/server/handlers/config.go b/server/handlers/config.go index 69d7fef..6ae809f 100644 --- a/server/handlers/config.go +++ b/server/handlers/config.go @@ -26,7 +26,7 @@ func AgentConfigHandler(database *db.DB, pushConfig func(agentID string, cfg *mo return } if cfg == nil { - cfg = &models.AgentConfig{} + cfg = models.DefaultAgentConfig() } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(cfg) diff --git a/server/main.go b/server/main.go index 729f414..161ac2d 100644 --- a/server/main.go +++ b/server/main.go @@ -110,6 +110,8 @@ func main() { handlers.IconUploadHandler(database)(w, r) 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) default: http.NotFound(w, r) } diff --git a/server/models/models.go b/server/models/models.go index 39759ff..ae9433d 100644 --- a/server/models/models.go +++ b/server/models/models.go @@ -88,6 +88,20 @@ type ServerConfig struct { PopupDetailH int `json:"popup_detail_h"` } +func DefaultAgentConfig() *AgentConfig { + on := MetricProto{UDP: true, MQTT: false} + return &AgentConfig{ + Protocols: ProtocolsConfig{UDP: UDPConfig{Enabled: true}}, + Metrics: MetricsConfig{ + CPU: on, + Memory: on, + Disk: on, + Smart: on, + Uptime: on, + }, + } +} + func DefaultServerConfig() ServerConfig { return ServerConfig{ TileMinWidth: 220, FontSize: 13,