feat: métriques réseau+hardware serveur+dashboard + API /agents/{id} + iperf3
Serveur:
- Modèles Go: NetworkInterface, HardwareInfo dans Agent + AgentMetrics
- DB: migrations network_info_json + hardware_info_json dans agents
- UpsertAgent: stocke les données lentes si présentes dans le payload
- GetAgents: désérialise network_info_json + hardware_info_json
- GET /api/agents/{id}: endpoint single agent
- docker-compose: service iperf3 (port 5201)
Dashboard:
- Popup détail: section RÉSEAU (tableau interfaces: type, vitesse, MAC, WoL, iperf3)
- Popup détail: section HARDWARE (carte mère, CPU, RAM slots/type/vitesse)
- CSS: .net-table/.net-row pour le tableau réseau
- Font-size global appliqué sur html root (au lieu de body)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
0430c0f2a8
commit
ff6cf1cd5e
@@ -279,5 +279,12 @@ body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-s
|
|||||||
border-radius:6px;background:var(--bg-3);border:1px solid var(--border-1)}
|
border-radius:6px;background:var(--bg-3);border:1px solid var(--border-1)}
|
||||||
.attr-ok{color:var(--ok)}
|
.attr-ok{color:var(--ok)}
|
||||||
|
|
||||||
|
/* Réseau + Hardware */
|
||||||
|
.net-table{display:flex;flex-direction:column;gap:3px}
|
||||||
|
.net-row{display:grid;grid-template-columns:18px 1fr 56px 130px 90px 90px;
|
||||||
|
align-items:center;gap:8px;padding:6px 10px;
|
||||||
|
background:var(--bg-3);border-radius:6px;border:1px solid var(--border-1);
|
||||||
|
font-family:var(--font-terminal);font-size:10px;color:var(--ink-2)}
|
||||||
|
|
||||||
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg-1)}
|
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg-1)}
|
||||||
::-webkit-scrollbar-thumb{background:var(--bg-4);border-radius:3px}
|
::-webkit-scrollbar-thumb{background:var(--bg-4);border-radius:3px}
|
||||||
|
|||||||
+1
-1
@@ -109,7 +109,7 @@ const App = (() => {
|
|||||||
document.documentElement.style.setProperty('--tile-min', _serverConfig.tile_min_width + 'px');
|
document.documentElement.style.setProperty('--tile-min', _serverConfig.tile_min_width + 'px');
|
||||||
}
|
}
|
||||||
if (_serverConfig.font_size) {
|
if (_serverConfig.font_size) {
|
||||||
document.body.style.fontSize = _serverConfig.font_size + 'px';
|
document.documentElement.style.fontSize = _serverConfig.font_size + 'px';
|
||||||
}
|
}
|
||||||
if (_serverConfig.popup_detail_w && _serverConfig.popup_detail_h) {
|
if (_serverConfig.popup_detail_w && _serverConfig.popup_detail_h) {
|
||||||
const pd = document.getElementById('popup-detail');
|
const pd = document.getElementById('popup-detail');
|
||||||
|
|||||||
@@ -151,6 +151,47 @@ const Popups = (() => {
|
|||||||
${smartBadges}
|
${smartBadges}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${(() => {
|
||||||
|
const ni = entry?.agent?.network_info;
|
||||||
|
if (!ni?.length) return '';
|
||||||
|
const wol = v => v == null ? '—' : v ? '<span style="color:var(--ok)">Oui</span>' : '<span style="color:var(--ink-4)">Non</span>';
|
||||||
|
const spd = v => v == null ? '—' : v >= 1000 ? '1 Gb' : v + ' Mb';
|
||||||
|
const rows = ni.map(iface => `
|
||||||
|
<div class="net-row">
|
||||||
|
<span style="color:var(--ink-3);font-size:12px"><i class="fa-solid fa-${iface.if_type === 'wifi' ? 'wifi' : 'ethernet'}"></i></span>
|
||||||
|
<span style="color:var(--ink-1);font-weight:600">${esc(iface.name)}</span>
|
||||||
|
<span style="color:var(--ink-3)">${spd(iface.speed_mbps)}</span>
|
||||||
|
<span style="color:var(--ink-4);font-size:9px;letter-spacing:.04em">${esc(iface.mac)}</span>
|
||||||
|
<span>WoL : ${wol(iface.wol)}</span>
|
||||||
|
<span style="color:var(--blue)">${iface.iperf_mbps != null ? iface.iperf_mbps.toFixed(1) + ' Mb/s' : '—'}</span>
|
||||||
|
</div>`).join('');
|
||||||
|
return `<div>
|
||||||
|
<div class="sec-title">RÉSEAU</div>
|
||||||
|
<div class="net-table">
|
||||||
|
<div class="net-row" style="background:var(--bg-4);font-size:9px;color:var(--ink-4);letter-spacing:.06em">
|
||||||
|
<span></span><span>INTERFACE</span><span>VITESSE</span><span>MAC</span><span>WAKE ON LAN</span><span>IPERF3</span>
|
||||||
|
</div>
|
||||||
|
${rows}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
})()}
|
||||||
|
${(() => {
|
||||||
|
const hw = entry?.agent?.hardware_info;
|
||||||
|
if (!hw) return '';
|
||||||
|
const row = (lbl, val) => val ? `<div class="meta"><div class="meta-lbl">${lbl}</div><div class="meta-val">${esc(String(val))}</div></div>` : '';
|
||||||
|
const ramSlots = hw.ram_slots_used != null && hw.ram_slots_total != null
|
||||||
|
? `${hw.ram_slots_used}/${hw.ram_slots_total} slots` : null;
|
||||||
|
const ramInfo = [hw.ram_type, hw.ram_speed_mhz ? hw.ram_speed_mhz + ' MHz' : null, ramSlots]
|
||||||
|
.filter(Boolean).join(' · ') || null;
|
||||||
|
return `<div>
|
||||||
|
<div class="sec-title">HARDWARE</div>
|
||||||
|
<div class="meta-grid">
|
||||||
|
${row('CARTE MÈRE', hw.motherboard_vendor && hw.motherboard_model ? hw.motherboard_vendor + ' ' + hw.motherboard_model : hw.motherboard_model || hw.motherboard_vendor)}
|
||||||
|
${row('PROCESSEUR', hw.cpu_model)}
|
||||||
|
${row('MÉMOIRE RAM', ramInfo)}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
})()}
|
||||||
<div>
|
<div>
|
||||||
<div class="sec-title">INFORMATIONS</div>
|
<div class="sec-title">INFORMATIONS</div>
|
||||||
<div class="meta-grid">
|
<div class="meta-grid">
|
||||||
|
|||||||
+31
-6
@@ -68,6 +68,8 @@ func (d *DB) migrate() error {
|
|||||||
_, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_hours INTEGER`)
|
_, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_hours INTEGER`)
|
||||||
_, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_wear INTEGER`)
|
_, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_wear INTEGER`)
|
||||||
_, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_json TEXT`)
|
_, _ = d.conn.Exec(`ALTER TABLE metrics ADD COLUMN smart_json TEXT`)
|
||||||
|
_, _ = d.conn.Exec(`ALTER TABLE agents ADD COLUMN network_info_json TEXT`)
|
||||||
|
_, _ = d.conn.Exec(`ALTER TABLE agents ADD COLUMN hardware_info_json TEXT`)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,13 +77,27 @@ func (d *DB) Close() { _ = d.conn.Close() }
|
|||||||
|
|
||||||
func (d *DB) UpsertAgent(m *models.AgentMetrics) error {
|
func (d *DB) UpsertAgent(m *models.AgentMetrics) error {
|
||||||
ts := time.Now().Unix()
|
ts := time.Now().Unix()
|
||||||
|
var netJSON, hwJSON interface{}
|
||||||
|
if len(m.NetworkInfo) > 0 {
|
||||||
|
if b, err := json.Marshal(m.NetworkInfo); err == nil {
|
||||||
|
netJSON = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.HardwareInfo != nil {
|
||||||
|
if b, err := json.Marshal(m.HardwareInfo); err == nil {
|
||||||
|
hwJSON = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
_, err := d.conn.Exec(`
|
_, err := d.conn.Exec(`
|
||||||
INSERT INTO agents (id, hostname, ip, status, last_seen, version)
|
INSERT INTO agents (id, hostname, ip, status, last_seen, version,
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
network_info_json, hardware_info_json)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
ip=excluded.ip, status=excluded.status, last_seen=excluded.last_seen,
|
ip=excluded.ip, status=excluded.status, last_seen=excluded.last_seen,
|
||||||
version=CASE WHEN excluded.version != '' THEN excluded.version ELSE version END`,
|
version=CASE WHEN excluded.version != '' THEN excluded.version ELSE version END,
|
||||||
m.Hostname, m.Hostname, m.IP, m.Status, ts, m.Version)
|
network_info_json=CASE WHEN excluded.network_info_json IS NOT NULL THEN excluded.network_info_json ELSE network_info_json END,
|
||||||
|
hardware_info_json=CASE WHEN excluded.hardware_info_json IS NOT NULL THEN excluded.hardware_info_json ELSE hardware_info_json END`,
|
||||||
|
m.Hostname, m.Hostname, m.IP, m.Status, ts, m.Version, netJSON, hwJSON)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +125,8 @@ func (d *DB) InsertMetrics(m *models.AgentMetrics) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) GetAgents() ([]models.Agent, error) {
|
func (d *DB) GetAgents() ([]models.Agent, error) {
|
||||||
rows, err := d.conn.Query(`SELECT id, hostname, ip, status, last_seen, version FROM agents`)
|
rows, err := d.conn.Query(`SELECT id, hostname, ip, status, last_seen, version,
|
||||||
|
network_info_json, hardware_info_json FROM agents`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -117,9 +134,17 @@ func (d *DB) GetAgents() ([]models.Agent, error) {
|
|||||||
var agents []models.Agent
|
var agents []models.Agent
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var a models.Agent
|
var a models.Agent
|
||||||
if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Status, &a.LastSeen, &a.Version); err != nil {
|
var netJSON, hwJSON *string
|
||||||
|
if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Status, &a.LastSeen, &a.Version,
|
||||||
|
&netJSON, &hwJSON); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if netJSON != nil {
|
||||||
|
_ = json.Unmarshal([]byte(*netJSON), &a.NetworkInfo)
|
||||||
|
}
|
||||||
|
if hwJSON != nil {
|
||||||
|
_ = json.Unmarshal([]byte(*hwJSON), &a.HardwareInfo)
|
||||||
|
}
|
||||||
agents = append(agents, a)
|
agents = append(agents, a)
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
|
|||||||
@@ -29,5 +29,14 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- server
|
- server
|
||||||
|
|
||||||
|
iperf3:
|
||||||
|
image: ${IPERF3_IMAGE:-public.ecr.aws/docker/library/networkstatic/iperf3:latest}
|
||||||
|
pull_policy: if_not_present
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["-s"]
|
||||||
|
ports:
|
||||||
|
- "5201:5201/tcp"
|
||||||
|
- "5201:5201/udp"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
nanometrics_data:
|
nanometrics_data:
|
||||||
|
|||||||
@@ -24,6 +24,31 @@ func AgentsHandler(database *db.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AgentDetailHandler(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]
|
||||||
|
agents, err := database.GetAgents()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, a := range agents {
|
||||||
|
if a.ID == agentID {
|
||||||
|
a.LastMetrics, _ = database.GetLastMetrics(agentID)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(a)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteAgentHandler(database *db.DB, broadcast func(interface{})) 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, "/"), "/")
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ func main() {
|
|||||||
handlers.IconGetHandler(database)(w, r)
|
handlers.IconGetHandler(database)(w, r)
|
||||||
case r.Method == http.MethodDelete:
|
case r.Method == http.MethodDelete:
|
||||||
handlers.DeleteAgentHandler(database, hub.Broadcast)(w, r)
|
handlers.DeleteAgentHandler(database, hub.Broadcast)(w, r)
|
||||||
|
case r.Method == http.MethodGet:
|
||||||
|
handlers.AgentDetailHandler(database)(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-23
@@ -1,22 +1,43 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
type AgentMetrics struct {
|
type AgentMetrics struct {
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
CPUPercent *float64 `json:"cpu_percent"`
|
CPUPercent *float64 `json:"cpu_percent"`
|
||||||
MemoryUsed *int64 `json:"memory_used"`
|
MemoryUsed *int64 `json:"memory_used"`
|
||||||
MemoryFree *int64 `json:"memory_free"`
|
MemoryFree *int64 `json:"memory_free"`
|
||||||
MemoryTotal *int64 `json:"memory_total"`
|
MemoryTotal *int64 `json:"memory_total"`
|
||||||
HDDUsed *int64 `json:"hdd_used"`
|
HDDUsed *int64 `json:"hdd_used"`
|
||||||
HDDFree *int64 `json:"hdd_free"`
|
HDDFree *int64 `json:"hdd_free"`
|
||||||
HDDTotal *int64 `json:"hdd_total"`
|
HDDTotal *int64 `json:"hdd_total"`
|
||||||
Uptime *int64 `json:"uptime"`
|
Uptime *int64 `json:"uptime"`
|
||||||
NetworkRX *int64 `json:"network_rx"`
|
NetworkRX *int64 `json:"network_rx"`
|
||||||
NetworkTX *int64 `json:"network_tx"`
|
NetworkTX *int64 `json:"network_tx"`
|
||||||
Temperature *float64 `json:"temperature"`
|
Temperature *float64 `json:"temperature"`
|
||||||
Smart []SmartMetrics `json:"smart"`
|
Smart []SmartMetrics `json:"smart"`
|
||||||
|
NetworkInfo []NetworkInterface `json:"network_info"`
|
||||||
|
HardwareInfo *HardwareInfo `json:"hardware_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkInterface struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
IfType string `json:"if_type"`
|
||||||
|
SpeedMbps *int64 `json:"speed_mbps"`
|
||||||
|
MAC string `json:"mac"`
|
||||||
|
WoL *bool `json:"wol"`
|
||||||
|
IperfMbps *float64 `json:"iperf_mbps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HardwareInfo struct {
|
||||||
|
MotherboardVendor *string `json:"motherboard_vendor"`
|
||||||
|
MotherboardModel *string `json:"motherboard_model"`
|
||||||
|
CPUModel *string `json:"cpu_model"`
|
||||||
|
RAMType *string `json:"ram_type"`
|
||||||
|
RAMSpeedMHz *int64 `json:"ram_speed_mhz"`
|
||||||
|
RAMSlotsUsed *int64 `json:"ram_slots_used"`
|
||||||
|
RAMSlotsTotal *int64 `json:"ram_slots_total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SmartMetrics struct {
|
type SmartMetrics struct {
|
||||||
@@ -29,13 +50,15 @@ type SmartMetrics struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
LastSeen int64 `json:"last_seen"`
|
LastSeen int64 `json:"last_seen"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
LastMetrics *AgentMetrics `json:"last_metrics,omitempty"`
|
LastMetrics *AgentMetrics `json:"last_metrics,omitempty"`
|
||||||
|
NetworkInfo []NetworkInterface `json:"network_info,omitempty"`
|
||||||
|
HardwareInfo *HardwareInfo `json:"hardware_info,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentConfig struct {
|
type AgentConfig struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user