diff --git a/dashboard/css/app.css b/dashboard/css/app.css index f10fb3e..cafea9c 100644 --- a/dashboard/css/app.css +++ b/dashboard/css/app.css @@ -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)} .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-thumb{background:var(--bg-4);border-radius:3px} diff --git a/dashboard/js/app.js b/dashboard/js/app.js index 829e626..afe6022 100644 --- a/dashboard/js/app.js +++ b/dashboard/js/app.js @@ -109,7 +109,7 @@ const App = (() => { document.documentElement.style.setProperty('--tile-min', _serverConfig.tile_min_width + 'px'); } 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) { const pd = document.getElementById('popup-detail'); diff --git a/dashboard/js/popups.js b/dashboard/js/popups.js index 476ee2b..d49f76f 100644 --- a/dashboard/js/popups.js +++ b/dashboard/js/popups.js @@ -151,6 +151,47 @@ const Popups = (() => { ${smartBadges} + ${(() => { + const ni = entry?.agent?.network_info; + if (!ni?.length) return ''; + const wol = v => v == null ? '—' : v ? 'Oui' : 'Non'; + const spd = v => v == null ? '—' : v >= 1000 ? '1 Gb' : v + ' Mb'; + const rows = ni.map(iface => ` +
+ + ${esc(iface.name)} + ${spd(iface.speed_mbps)} + ${esc(iface.mac)} + WoL : ${wol(iface.wol)} + ${iface.iperf_mbps != null ? iface.iperf_mbps.toFixed(1) + ' Mb/s' : '—'} +
`).join(''); + return `
+
RÉSEAU
+
+
+ INTERFACEVITESSEMACWAKE ON LANIPERF3 +
+ ${rows} +
+
`; + })()} + ${(() => { + const hw = entry?.agent?.hardware_info; + if (!hw) return ''; + const row = (lbl, val) => val ? `
${lbl}
${esc(String(val))}
` : ''; + 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 `
+
HARDWARE
+
+ ${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)} +
+
`; + })()}
INFORMATIONS
diff --git a/server/db/db.go b/server/db/db.go index 3f1ff11..21fe2e7 100644 --- a/server/db/db.go +++ b/server/db/db.go @@ -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 { diff --git a/server/docker-compose.yml b/server/docker-compose.yml index 7637887..12425f5 100644 --- a/server/docker-compose.yml +++ b/server/docker-compose.yml @@ -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: diff --git a/server/handlers/agents.go b/server/handlers/agents.go index 6566414..521b0dc 100644 --- a/server/handlers/agents.go +++ b/server/handlers/agents.go @@ -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, "/"), "/") diff --git a/server/main.go b/server/main.go index db252f3..9b626a4 100644 --- a/server/main.go +++ b/server/main.go @@ -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) } diff --git a/server/models/models.go b/server/models/models.go index e635f5c..2cb651e 100644 --- a/server/models/models.go +++ b/server/models/models.go @@ -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 {