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:
Gilles Soulier
2026-05-23 06:17:54 +02:00
parent 0430c0f2a8
commit ff6cf1cd5e
8 changed files with 162 additions and 30 deletions
+31 -6
View File
@@ -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_wear INTEGER`)
_, _ = 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
}
@@ -75,13 +77,27 @@ func (d *DB) Close() { _ = d.conn.Close() }
func (d *DB) UpsertAgent(m *models.AgentMetrics) error {
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(`
INSERT INTO agents (id, hostname, ip, status, last_seen, version)
VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO agents (id, hostname, ip, status, last_seen, version,
network_info_json, hardware_info_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
ip=excluded.ip, status=excluded.status, last_seen=excluded.last_seen,
version=CASE WHEN excluded.version != '' THEN excluded.version ELSE version END`,
m.Hostname, m.Hostname, m.IP, m.Status, ts, m.Version)
version=CASE WHEN excluded.version != '' THEN excluded.version ELSE version END,
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
}
@@ -109,7 +125,8 @@ func (d *DB) InsertMetrics(m *models.AgentMetrics) 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 {
return nil, err
}
@@ -117,9 +134,17 @@ func (d *DB) GetAgents() ([]models.Agent, error) {
var agents []models.Agent
for rows.Next() {
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
}
if netJSON != nil {
_ = json.Unmarshal([]byte(*netJSON), &a.NetworkInfo)
}
if hwJSON != nil {
_ = json.Unmarshal([]byte(*hwJSON), &a.HardwareInfo)
}
agents = append(agents, a)
}
if err := rows.Err(); err != nil {
+9
View File
@@ -29,5 +29,14 @@ services:
depends_on:
- 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:
nanometrics_data:
+25
View File
@@ -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 {
return func(w http.ResponseWriter, r *http.Request) {
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
+2
View File
@@ -115,6 +115,8 @@ func main() {
handlers.IconGetHandler(database)(w, r)
case r.Method == http.MethodDelete:
handlers.DeleteAgentHandler(database, hub.Broadcast)(w, r)
case r.Method == http.MethodGet:
handlers.AgentDetailHandler(database)(w, r)
default:
http.NotFound(w, r)
}
+46 -23
View File
@@ -1,22 +1,43 @@
package models
type AgentMetrics struct {
Hostname string `json:"hostname"`
IP string `json:"ip"`
Status string `json:"status"`
Version string `json:"version"`
CPUPercent *float64 `json:"cpu_percent"`
MemoryUsed *int64 `json:"memory_used"`
MemoryFree *int64 `json:"memory_free"`
MemoryTotal *int64 `json:"memory_total"`
HDDUsed *int64 `json:"hdd_used"`
HDDFree *int64 `json:"hdd_free"`
HDDTotal *int64 `json:"hdd_total"`
Uptime *int64 `json:"uptime"`
NetworkRX *int64 `json:"network_rx"`
NetworkTX *int64 `json:"network_tx"`
Temperature *float64 `json:"temperature"`
Smart []SmartMetrics `json:"smart"`
Hostname string `json:"hostname"`
IP string `json:"ip"`
Status string `json:"status"`
Version string `json:"version"`
CPUPercent *float64 `json:"cpu_percent"`
MemoryUsed *int64 `json:"memory_used"`
MemoryFree *int64 `json:"memory_free"`
MemoryTotal *int64 `json:"memory_total"`
HDDUsed *int64 `json:"hdd_used"`
HDDFree *int64 `json:"hdd_free"`
HDDTotal *int64 `json:"hdd_total"`
Uptime *int64 `json:"uptime"`
NetworkRX *int64 `json:"network_rx"`
NetworkTX *int64 `json:"network_tx"`
Temperature *float64 `json:"temperature"`
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 {
@@ -29,13 +50,15 @@ type SmartMetrics struct {
}
type Agent struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
IP string `json:"ip"`
Status string `json:"status"`
LastSeen int64 `json:"last_seen"`
Version string `json:"version,omitempty"`
LastMetrics *AgentMetrics `json:"last_metrics,omitempty"`
ID string `json:"id"`
Hostname string `json:"hostname"`
IP string `json:"ip"`
Status string `json:"status"`
LastSeen int64 `json:"last_seen"`
Version string `json:"version,omitempty"`
LastMetrics *AgentMetrics `json:"last_metrics,omitempty"`
NetworkInfo []NetworkInterface `json:"network_info,omitempty"`
HardwareInfo *HardwareInfo `json:"hardware_info,omitempty"`
}
type AgentConfig struct {