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}
|
.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);
|
.tile-foot{font-family:var(--font-terminal);font-size:10px;color:var(--ink-4);
|
||||||
display:flex;align-items:center;gap:5px;user-select:none}
|
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 */
|
||||||
.footer{background:var(--bg-0);border-top:1px solid var(--border-2);height:26px;
|
.footer{background:var(--bg-0);border-top:1px solid var(--border-2);height:26px;
|
||||||
|
|||||||
@@ -147,6 +147,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</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/api.js"></script>
|
||||||
<script src="js/charts.js"></script>
|
<script src="js/charts.js"></script>
|
||||||
<script src="js/grid.js"></script>
|
<script src="js/grid.js"></script>
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ const API = (() => {
|
|||||||
if (!r.ok) throw new Error(`PUT ${path}: ${r.status}`);
|
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) {
|
async function postForm(path, formData) {
|
||||||
const r = await fetch(BASE + path, { method: 'POST', body: formData });
|
const r = await fetch(BASE + path, { method: 'POST', body: formData });
|
||||||
if (!r.ok) throw new Error(`POST ${path}: ${r.status}`);
|
if (!r.ok) throw new Error(`POST ${path}: ${r.status}`);
|
||||||
@@ -44,6 +49,7 @@ const API = (() => {
|
|||||||
fd.append('icon', file);
|
fd.append('icon', file);
|
||||||
return postForm(`/api/agents/${id}/icon`, fd);
|
return postForm(`/api/agents/${id}/icon`, fd);
|
||||||
},
|
},
|
||||||
|
deleteAgent: (id) => del(`/api/agents/${id}`),
|
||||||
iconUrl: (id) => `/api/agents/${id}/icon`,
|
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-ico" data-tip="RAM"><i class="fa-solid fa-memory"></i></div>
|
||||||
<div class="g-bar"><div class="g-fill ${offline ? '' : gFill(memPct ?? 0)}"
|
<div class="g-bar"><div class="g-fill ${offline ? '' : gFill(memPct ?? 0)}"
|
||||||
style="width:${offline ? 0 : (memPct ?? 0).toFixed(0)}%"></div></div>
|
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>
|
||||||
<div class="g-row">
|
<div class="g-row">
|
||||||
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
|
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
|
||||||
@@ -94,6 +94,10 @@ const Grid = (() => {
|
|||||||
${offline
|
${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-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>`}
|
: `<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>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -139,6 +143,13 @@ const Grid = (() => {
|
|||||||
document.getElementById('stat-err').textContent = err;
|
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 getAgent(id) { return _agents.get(id); }
|
||||||
|
|
||||||
function updateStatus(agentId, status) {
|
function updateStatus(agentId, status) {
|
||||||
@@ -150,5 +161,5 @@ const Grid = (() => {
|
|||||||
updateStats();
|
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';
|
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 {
|
return {
|
||||||
showDetail, hideDetail,
|
showDetail, hideDetail,
|
||||||
showAgentCfg, sendAgentConfig, toggleCbox,
|
showAgentCfg, sendAgentConfig, toggleCbox,
|
||||||
showSrvCfg, hideSrvCfg, saveSrvCfg,
|
showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent,
|
||||||
showSmart,
|
showSmart,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -238,6 +238,20 @@ func (d *DB) MarkOffline(timeoutSec int64) error {
|
|||||||
return err
|
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.
|
// MarkOfflineAndGetIDs marque les agents inactifs et retourne leurs IDs.
|
||||||
func (d *DB) MarkOfflineAndGetIDs(timeoutSec int64) ([]string, error) {
|
func (d *DB) MarkOfflineAndGetIDs(timeoutSec int64) ([]string, error) {
|
||||||
cutoff := time.Now().Unix() - timeoutSec
|
cutoff := time.Now().Unix() - timeoutSec
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/user/nanometrics/server/db"
|
"github.com/user/nanometrics/server/db"
|
||||||
)
|
)
|
||||||
@@ -18,3 +19,19 @@ func AgentsHandler(database *db.DB) http.HandlerFunc {
|
|||||||
json.NewEncoder(w).Encode(agents)
|
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
|
return
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
cfg = &models.AgentConfig{}
|
cfg = models.DefaultAgentConfig()
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(cfg)
|
json.NewEncoder(w).Encode(cfg)
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ func main() {
|
|||||||
handlers.IconUploadHandler(database)(w, r)
|
handlers.IconUploadHandler(database)(w, r)
|
||||||
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:
|
||||||
|
handlers.DeleteAgentHandler(database)(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,20 @@ type ServerConfig struct {
|
|||||||
PopupDetailH int `json:"popup_detail_h"`
|
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 {
|
func DefaultServerConfig() ServerConfig {
|
||||||
return ServerConfig{
|
return ServerConfig{
|
||||||
TileMinWidth: 220, FontSize: 13,
|
TileMinWidth: 220, FontSize: 13,
|
||||||
|
|||||||
Reference in New Issue
Block a user