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 => `
+
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 {