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:
@@ -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}
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
@@ -151,6 +151,47 @@ const Popups = (() => {
|
||||
${smartBadges}
|
||||
</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 class="sec-title">INFORMATIONS</div>
|
||||
<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_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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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, "/"), "/")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,27 @@ type AgentMetrics struct {
|
||||
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 {
|
||||
@@ -36,6 +57,8 @@ type Agent struct {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user