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,