feat: suppression agent, RAM en Go, métriques par défaut (cpu/mem/disk/smart)
- API DELETE /api/agents/{id} — supprime agent + métriques + config + icône
- Bouton poubelle sur chaque tuile + dialog de confirmation
- RAM : affichage "utilisé/total" en Go (ex: 6.2Go/8.0Go) au lieu du %
- Config agent par défaut : cpu, memory, disk, smart activés (UDP)
- DefaultAgentConfig() dans models pour les nouveaux agents
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -147,6 +147,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DIALOG SUPPRESSION AGENT -->
|
||||
<div class="overlay" id="overlay-del" style="display:none;z-index:400" onclick="if(event.target===this)this.style.display='none'">
|
||||
<div class="popup" style="width:360px;max-width:96vw" onclick="event.stopPropagation()">
|
||||
<div style="padding:20px 20px 0">
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">
|
||||
<div style="width:36px;height:36px;border-radius:8px;background:color-mix(in srgb,var(--err) 15%,transparent);display:flex;align-items:center;justify-content:center;color:var(--err);font-size:16px;flex-shrink:0"><i class="fa-solid fa-triangle-exclamation"></i></div>
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:14px">Supprimer l'agent</div>
|
||||
<div style="font-size:12px;color:var(--fg2);margin-top:2px">Cette action est irréversible</div>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size:13px;margin:0 0 8px">Supprimer <strong id="del-agent-name">—</strong> ?<br>
|
||||
<span style="font-size:11px;color:var(--fg2)">Toutes les métriques historiques seront effacées.</span></p>
|
||||
</div>
|
||||
<div style="padding:16px 20px;display:flex;justify-content:flex-end;gap:8px">
|
||||
<button class="btn" onclick="document.getElementById('overlay-del').style.display='none'">Annuler</button>
|
||||
<button class="btn" id="del-agent-confirm"
|
||||
style="background:var(--err);color:#fff;border-color:var(--err)"
|
||||
onclick="Popups.doDeleteAgent()"><i class="fa-solid fa-trash"></i> Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js"></script>
|
||||
<script src="js/charts.js"></script>
|
||||
<script src="js/grid.js"></script>
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
})();
|
||||
|
||||
+13
-2
@@ -81,7 +81,7 @@ const Grid = (() => {
|
||||
<div class="g-ico" data-tip="RAM"><i class="fa-solid fa-memory"></i></div>
|
||||
<div class="g-bar"><div class="g-fill ${offline ? '' : gFill(memPct ?? 0)}"
|
||||
style="width:${offline ? 0 : (memPct ?? 0).toFixed(0)}%"></div></div>
|
||||
<span class="g-val">${offline ? '—' : fmtPct(memPct)}</span>
|
||||
<span class="g-val">${offline ? '—' : (metrics?.memory_used && metrics?.memory_total ? fmt(metrics.memory_used) + '/' + fmt(metrics.memory_total) : '—')}</span>
|
||||
</div>
|
||||
<div class="g-row">
|
||||
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
|
||||
@@ -94,6 +94,10 @@ const Grid = (() => {
|
||||
${offline
|
||||
? '<i class="fa-solid fa-circle-xmark" style="color:var(--err)"></i><span style="color:var(--err)">Hors ligne</span>'
|
||||
: `<i class="fa-solid fa-clock"></i><span>${uptimeStr}</span>`}
|
||||
<button class="btn-del-agent" title="Supprimer cet agent"
|
||||
onclick="event.stopPropagation();Popups.confirmDeleteAgent('${esc(id)}','${esc(agent.hostname)}')">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -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 };
|
||||
})();
|
||||
|
||||
+23
-1
@@ -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,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user