From dc60fe2a8da17872614723b9ab12e288ea18c14f Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Sat, 23 May 2026 07:36:06 +0200 Subject: [PATCH] 3 --- agent/Cargo.lock | 2 +- amelioration.md | 18 + capture/image1.png | Bin 0 -> 51875 bytes .../plans/2026-05-23-ameliorations.md | 644 ++++++++++++++++++ server/main.go | 2 +- server/transport/udp.go | 17 +- 6 files changed, 677 insertions(+), 6 deletions(-) create mode 100644 amelioration.md create mode 100644 capture/image1.png create mode 100644 docs/superpowers/plans/2026-05-23-ameliorations.md diff --git a/agent/Cargo.lock b/agent/Cargo.lock index 9e45275..8f32a8d 100644 --- a/agent/Cargo.lock +++ b/agent/Cargo.lock @@ -248,7 +248,7 @@ dependencies = [ [[package]] name = "nanometrics-agent" -version = "0.1.6" +version = "0.1.10" dependencies = [ "libc", "rumqttc", diff --git a/amelioration.md b/amelioration.md new file mode 100644 index 0000000..e81bc29 --- /dev/null +++ b/amelioration.md @@ -0,0 +1,18 @@ +- metric du reseau: se concentrer uniquement sur les cartes reseaux appartenant a mon reseau local, les item interressant c est nom de l interface, type 10/100/1000mb, eth ou wifi, wake on lan actif ? macaddress, resultat de mesure d'un iperf avec un serveur (le serveur sera installe dans le compose deja creer pour l app serveur, c es metric ne sont recuprer qu au demarrage de l agent puis une fois/jours et seront visible dans le popup de la tuile +- metric hardware, revupere des info sur carte mere, type de ram, type de cpu ( via un dmidecode ou similaire) ces données seront lu un fois au demarrage de l agent puis une fois par jours +- le script et l agent doit etre installable sur un proxmox, verifie si les metric seront bien ok ? surtout les diques durs +- reglage de la taille des caractere valable sur toute l ui du frontend +- les data seront accessible via api rest pour autre service ou verveur mcp +- les parametre du fichier de config seront exporte vers le serveur , et via config de le tuile, pourront etre renvoyer vers l agent +- lors du script d installation, affiche la version de l agent installe +- dans le pop up la ram est affiche en % seulement, ajoute le metric en Go +- verifie que le devellopement de l agent est modulaire et optimise +- ajouter en metric le nom des 4 processus qui consomme le plus de ressource +- pour l agent une option debug ( activable via l'interrface de config de la tuile permet de generer un log des metric recuperer)quels commande pour visualiser le metric ? +- pouvoir relancer le service depuis ler serveur +- le site https://github.com/nicolargo/glances peut tu faire une analyse approfondi des metric relevé, des techno utilisé et me dire les similitude et difference avec mon projet ( créer un fichier comparatif_glance.md ) et synthese finale tu pourrais proposer des amelioration de mon outils qui pourrait s'inspirer de cette app, => amelioration_brainstormind.md +- lors de l'installation d'iperf3 j'ai ce message: Choisissez cette option si Iperf3 doit démarrer automatiquement en tant que démon, maintenant et au démarrage. │ + │ │ + │ Faut-il démarrer automatiquement Iperf3 en tant que démon ? │ + │ │ + │ , peut on faire une installe silencieuse pour le script des agent en repondant non \ No newline at end of file diff --git a/capture/image1.png b/capture/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..9182ad5ac649ee2de21e1a4946e4c07bec65b6f1 GIT binary patch literal 51875 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%0A?0?n7VM2l*-%hueGYy^ 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 literal 0 HcmV?d00001 diff --git a/docs/superpowers/plans/2026-05-23-ameliorations.md b/docs/superpowers/plans/2026-05-23-ameliorations.md new file mode 100644 index 0000000..1c886cd --- /dev/null +++ b/docs/superpowers/plans/2026-05-23-ameliorations.md @@ -0,0 +1,644 @@ +# Améliorations Nanometrics — Plan d'implémentation + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ajouter métriques réseau enrichies, hardware, config bidirectionnelle, API REST complète, taille police globale. + +**Architecture:** +- Métriques lentes (réseau, hardware) : collecte au démarrage + une fois/jour à heure fixe (config `slow_daily_time`) +- Stockage dans la table `agents` (colonnes JSON), pas dans `metrics` — ces données changent rarement +- API REST expose tout via les mêmes endpoints enrichis + +**Tech Stack:** Rust (agent), Go (server), SQLite, Vanilla JS (dashboard) + +--- + +## Fichiers concernés + +| Fichier | Action | +|---------|--------| +| `agent/src/payload.rs` | Ajout `NetworkInterface`, `HardwareInfo`, champs dans `AgentMetrics` | +| `agent/src/config.rs` | Ajout `slow_daily_time`, `network_info`, `hardware_info` dans `MetricsConfig` | +| `agent/src/metrics/network_info.rs` | Nouveau module | +| `agent/src/metrics/hardware.rs` | Nouveau module | +| `agent/src/metrics/mod.rs` | Déclarer les 2 nouveaux modules | +| `agent/src/main.rs` | Intégration scheduler, collecte slow | +| `agent/Cargo.toml` | Bump version 0.1.6 | +| `deploy/install.sh` | Ajout `iperf3`, `dmidecode` dans paquets | +| `server/models/models.go` | Structs Go `NetworkInterface`, `HardwareInfo` | +| `server/db/db.go` | Migrations + `UpsertAgent` + `GetLastMetrics` | +| `server/handlers/agents.go` | Handler GET `/api/agents/{id}` | +| `server/main.go` | Route `/api/agents/{id}` | +| `server/docker-compose.yml` | Service iperf3 | +| `dashboard/js/popups.js` | Sections réseau + hardware dans popup détail | +| `dashboard/css/app.css` | Styles network/hardware section + fix font-size global | +| `dashboard/js/app.js` | Fix font-size sur `html` element | + +--- + +## Task 1 — Agent : structs payload + config + +**Files:** +- Modify: `agent/src/payload.rs` +- Modify: `agent/src/config.rs` + +- [ ] **Ajouter dans `payload.rs`** les nouveaux types et champs : + +```rust +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct NetworkInterface { + pub name: String, + pub if_type: String, // "ethernet" | "wifi" + pub speed_mbps: Option, + pub mac: String, + pub wol: Option, + pub iperf_mbps: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct HardwareInfo { + pub motherboard_vendor: Option, + pub motherboard_model: Option, + pub cpu_model: Option, + pub ram_type: Option, + pub ram_speed_mhz: Option, + pub ram_slots_used: Option, + pub ram_slots_total: Option, +} +``` + +Dans `AgentMetrics`, ajouter après `smart` : +```rust +pub network_info: Option>, +pub hardware_info: Option, +``` + +- [ ] **Ajouter dans `config.rs`** — `SlowMetricsConfig` + champs dans `MetricsConfig` : + +```rust +#[derive(Deserialize, Debug, Clone)] +pub struct SlowMetricsConfig { + #[serde(default)] + pub udp: bool, + #[serde(default)] + pub mqtt: bool, +} + +impl Default for SlowMetricsConfig { + fn default() -> Self { Self { udp: true, mqtt: false } } +} +``` + +Dans `MetricsConfig`, ajouter : +```rust +#[serde(default)] +pub network_info: SlowMetricsConfig, +#[serde(default)] +pub hardware_info: SlowMetricsConfig, +#[serde(default = "default_slow_time")] +pub slow_daily_time: String, // "HH:MM" +``` + +```rust +fn default_slow_time() -> String { "03:00".to_string() } +``` + +- [ ] **Vérifier** : `cargo check --manifest-path agent/Cargo.toml` + +- [ ] **Commit** : +```bash +git add agent/src/payload.rs agent/src/config.rs +git commit -m "feat(agent): structs NetworkInterface + HardwareInfo + config slow_daily_time" +``` + +--- + +## Task 2 — Agent : module network_info + +**Files:** +- Create: `agent/src/metrics/network_info.rs` +- Modify: `agent/src/metrics/mod.rs` + +- [ ] **Créer `agent/src/metrics/network_info.rs`** : + +```rust +use std::mem::MaybeUninit; + +fn local_hhmm() -> (u32, u32) { + let now = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + let mut tm = MaybeUninit::::uninit(); + unsafe { libc::localtime_r(&now, tm.as_mut_ptr()) }; + let tm = unsafe { tm.assume_init() }; + (tm.tm_hour as u32, tm.tm_min as u32) +} + +pub fn current_hhmm() -> (u32, u32) { local_hhmm() } + +fn is_physical(name: &str) -> bool { + // Exclure loopback, virtuels, docker, bridges + if name == "lo" { return false; } + for prefix in &["veth", "docker", "br-", "virbr", "vir", "tun", "tap", "dummy"] { + if name.starts_with(prefix) { return false; } + } + true +} + +fn read_sysfs(iface: &str, file: &str) -> Option { + std::fs::read_to_string(format!("/sys/class/net/{}/{}", iface, file)) + .ok() + .map(|s| s.trim().to_string()) +} + +fn is_wifi(name: &str) -> bool { + std::path::Path::new(&format!("/sys/class/net/{}/wireless", name)).exists() +} + +fn wol_status(name: &str) -> Option { + let out = std::process::Command::new("ethtool") + .arg(name).output().ok()?; + let text = String::from_utf8_lossy(&out.stdout); + for line in text.lines() { + let t = line.trim(); + if t.starts_with("Wake-on:") { + let val = t.split(':').nth(1)?.trim(); + return Some(val != "d" && !val.is_empty()); + } + } + None +} + +fn iperf_mbps(server_ip: &str) -> Option { + // Vérifier que iperf3 est disponible + if !std::process::Command::new("which").arg("iperf3") + .output().map(|o| o.status.success()).unwrap_or(false) { + return None; + } + let out = std::process::Command::new("iperf3") + .args(["-c", server_ip, "-J", "-t", "5", "-P", "1"]) + .output().ok()?; + let json = String::from_utf8_lossy(&out.stdout); + // parser "end" > "sum_received" > "bits_per_second" + let v: serde_json::Value = serde_json::from_str(&json).ok()?; + let bps = v["end"]["sum_received"]["bits_per_second"].as_f64()?; + Some(bps / 1_000_000.0) +} + +pub fn collect(server_ip: &str) -> Vec { + let entries = match std::fs::read_dir("/sys/class/net") { + Ok(e) => e, Err(_) => return vec![], + }; + let mut ifaces: Vec = entries + .flatten() + .map(|e| e.file_name().into_string().unwrap_or_default()) + .filter(|n| is_physical(n)) + .collect(); + ifaces.sort(); + + // Lancer iperf une seule fois pour tous (pas par interface) + let iperf = iperf_mbps(server_ip); + + ifaces.iter().map(|name| { + let speed = read_sysfs(name, "speed") + .and_then(|s| s.parse::().ok()) + .filter(|&v| v > 0); + let mac = read_sysfs(name, "address").unwrap_or_default(); + crate::payload::NetworkInterface { + name: name.clone(), + if_type: if is_wifi(name) { "wifi".to_string() } else { "ethernet".to_string() }, + speed_mbps: speed, + mac, + wol: if is_wifi(name) { None } else { wol_status(name) }, + iperf_mbps: iperf, + } + }).collect() +} +``` + +- [ ] **Ajouter dans `agent/src/metrics/mod.rs`** : `pub mod network_info;` + +- [ ] **Vérifier** : `cargo check --manifest-path agent/Cargo.toml` + +- [ ] **Commit** : +```bash +git add agent/src/metrics/network_info.rs agent/src/metrics/mod.rs +git commit -m "feat(agent): module network_info (interfaces, WoL, iperf3)" +``` + +--- + +## Task 3 — Agent : module hardware + +**Files:** +- Create: `agent/src/metrics/hardware.rs` +- Modify: `agent/src/metrics/mod.rs` + +- [ ] **Créer `agent/src/metrics/hardware.rs`** : + +```rust +fn run_dmidecode(type_num: u8) -> String { + std::process::Command::new("dmidecode") + .args(["-t", &type_num.to_string()]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).into_owned()) + .unwrap_or_default() +} + +fn extract_field<'a>(text: &'a str, key: &str) -> Option { + for line in text.lines() { + let t = line.trim(); + if t.starts_with(key) { + let val = t[key.len()..].trim().trim_start_matches(':').trim(); + if !val.is_empty() && val != "Not Specified" && val != "Unknown" { + return Some(val.to_string()); + } + } + } + None +} + +pub fn is_available() -> bool { + std::process::Command::new("which").arg("dmidecode") + .output().map(|o| o.status.success()).unwrap_or(false) +} + +pub fn collect() -> Option { + if !is_available() { return None; } + + // Type 2 = Baseboard, Type 4 = Processor, Type 17 = Memory Device + let board = run_dmidecode(2); + let cpu = run_dmidecode(4); + let mem = run_dmidecode(17); + + let mut slots_total: i64 = 0; + let mut slots_used: i64 = 0; + let mut ram_type: Option = None; + let mut ram_speed: Option = None; + + // Compter les slots mémoire + for block in mem.split("\n\n") { + if block.contains("Memory Device") { + slots_total += 1; + if let Some(size) = extract_field(block, "Size") { + if !size.contains("No Module") { + slots_used += 1; + } + } + if ram_type.is_none() { + ram_type = extract_field(block, "Type"); + } + if ram_speed.is_none() { + if let Some(spd) = extract_field(block, "Speed") { + // "3200 MT/s" → 3200 + ram_speed = spd.split_whitespace().next() + .and_then(|s| s.parse().ok()); + } + } + } + } + + Some(crate::payload::HardwareInfo { + motherboard_vendor: extract_field(&board, "Manufacturer"), + motherboard_model: extract_field(&board, "Product Name"), + cpu_model: extract_field(&cpu, "Version"), + ram_type, + ram_speed_mhz: ram_speed, + ram_slots_used: if slots_total > 0 { Some(slots_used) } else { None }, + ram_slots_total: if slots_total > 0 { Some(slots_total) } else { None }, + }) +} +``` + +- [ ] **Ajouter dans `agent/src/metrics/mod.rs`** : `pub mod hardware;` + +- [ ] **Vérifier** : `cargo check --manifest-path agent/Cargo.toml` + +- [ ] **Commit** : +```bash +git add agent/src/metrics/hardware.rs agent/src/metrics/mod.rs +git commit -m "feat(agent): module hardware (dmidecode — carte mère, CPU, RAM)" +``` + +--- + +## Task 4 — Agent : scheduler + intégration main.rs + install.sh + version + +**Files:** +- Modify: `agent/src/main.rs` +- Modify: `agent/Cargo.toml` +- Modify: `deploy/install.sh` + +- [ ] **Bump version** dans `agent/Cargo.toml` : `0.1.5` → `0.1.6` + +- [ ] **Ajouter dans `deploy/install.sh`** les paquets `iperf3` et `dmidecode` : +```bash +for pkg in curl python3 smartmontools ethtool iperf3 dmidecode; do +``` + +- [ ] **Ajouter dans `agent/src/main.rs`** le scheduler slow + appels modules. Après les variables `first_slow` / `last_slow`, ajouter : + +```rust +// Scheduler métriques lentes (startup + 1×/jour à l'heure configurée) +let slow_time: (u32, u32) = { + let parts: Vec<&str> = cfg.metrics.slow_daily_time.split(':').collect(); + let h = parts.get(0).and_then(|s| s.parse().ok()).unwrap_or(3); + let m = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + (h, m) +}; +let mut slow_daily_done = false; +let mut slow_last_date: u32 = 0; // tm_yday pour détecter changement de jour + +// Collecte immédiate au démarrage +if cfg.metrics.network_info.udp || cfg.metrics.network_info.mqtt { + let ni = metrics::network_info::collect(&cfg.server.ip); + if !ni.is_empty() { m.network_info = Some(ni); } +} +if cfg.metrics.hardware_info.udp || cfg.metrics.hardware_info.mqtt { + m.hardware_info = metrics::hardware::collect(); +} +``` + +Dans la boucle principale, ajouter la vérification de l'heure après le bloc `first_slow` : + +```rust +// Métriques lentes quotidiennes +{ + use std::mem::MaybeUninit; + let now_ts = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap_or_default().as_secs() as i64; + let mut tm = MaybeUninit::::uninit(); + unsafe { libc::localtime_r(&now_ts, tm.as_mut_ptr()) }; + let tm = unsafe { tm.assume_init() }; + let (cur_h, cur_m) = (tm.tm_hour as u32, tm.tm_min as u32); + let cur_yday = tm.tm_yday as u32; + + if cur_yday != slow_last_date { + slow_last_date = cur_yday; + slow_daily_done = false; + } + if !slow_daily_done && cur_h == slow_time.0 && cur_m == slow_time.1 { + slow_daily_done = true; + if cfg.metrics.network_info.udp || cfg.metrics.network_info.mqtt { + let ni = metrics::network_info::collect(&cfg.server.ip); + if !ni.is_empty() { m.network_info = Some(ni); } + } + if cfg.metrics.hardware_info.udp || cfg.metrics.hardware_info.mqtt { + m.hardware_info = metrics::hardware::collect(); + } + } +} +``` + +- [ ] **Vérifier** : `cargo check --manifest-path agent/Cargo.toml` + +- [ ] **Commit** : +```bash +git add agent/src/main.rs agent/Cargo.toml deploy/install.sh +git commit -m "feat(agent v0.1.6): scheduler slow metrics + réseau + hardware + iperf3/dmidecode dans install.sh" +``` + +--- + +## Task 5 — Serveur : modèles Go + migrations DB + stockage + +**Files:** +- Modify: `server/models/models.go` +- Modify: `server/db/db.go` + +- [ ] **Ajouter dans `server/models/models.go`** : + +```go +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"` +} +``` + +Dans `AgentMetrics`, ajouter : +```go +NetworkInfo []NetworkInterface `json:"network_info"` +HardwareInfo *HardwareInfo `json:"hardware_info"` +``` + +Dans `Agent`, ajouter : +```go +NetworkInfo []NetworkInterface `json:"network_info,omitempty"` +HardwareInfo *HardwareInfo `json:"hardware_info,omitempty"` +``` + +- [ ] **Dans `server/db/db.go`** — migrations : + +Dans `migrate()`, ajouter : +```go +_, _ = d.conn.Exec(`ALTER TABLE agents ADD COLUMN network_info_json TEXT`) +_, _ = d.conn.Exec(`ALTER TABLE agents ADD COLUMN hardware_info_json TEXT`) +``` + +- [ ] **Dans `UpsertAgent()`** — stocker les données lentes si présentes : + +```go +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 (?, ?, ?, ?, ?, ?) + 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, + network_info_json=CASE WHEN ?7 IS NOT NULL THEN ?7 ELSE network_info_json END, + hardware_info_json=CASE WHEN ?8 IS NOT NULL THEN ?8 ELSE hardware_info_json END`, + m.Hostname, m.Hostname, m.IP, m.Status, ts, m.Version, netJSON, hwJSON) + return err +} +``` + +- [ ] **Dans `GetAgents()`** — lire et désérialiser les colonnes JSON : + +```go +func (d *DB) GetAgents() ([]models.Agent, error) { + rows, err := d.conn.Query(`SELECT id, hostname, ip, status, last_seen, version, + network_info_json, hardware_info_json FROM agents`) + // ... + var netJSON, hwJSON *string + if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Status, &a.LastSeen, &a.Version, + &netJSON, &hwJSON); err != nil { ... } + if netJSON != nil { _ = json.Unmarshal([]byte(*netJSON), &a.NetworkInfo) } + if hwJSON != nil { _ = json.Unmarshal([]byte(*hwJSON), &a.HardwareInfo) } +} +``` + +- [ ] **Vérifier** : `cd server && go build ./...` + +- [ ] **Commit** : +```bash +git add server/models/models.go server/db/db.go +git commit -m "feat(server): NetworkInterface + HardwareInfo — migration DB + stockage agents" +``` + +--- + +## Task 6 — Serveur : API GET /api/agents/{id} + docker-compose iperf3 + +**Files:** +- Modify: `server/handlers/agents.go` +- Modify: `server/main.go` +- Modify: `server/docker-compose.yml` + +- [ ] **Ajouter dans `server/handlers/agents.go`** le handler single agent : + +```go +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) + } +} +``` + +- [ ] **Dans `server/main.go`** — ajouter la route dans le switch `/api/agents/` : + +```go +case r.Method == http.MethodGet && !strings.HasSuffix(r.URL.Path, "/"): + handlers.AgentDetailHandler(database)(w, r) +``` + +- [ ] **Dans `server/docker-compose.yml`** — ajouter le service iperf3 : + +```yaml + 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" +``` + +- [ ] **Vérifier** : `cd server && go build ./...` + +- [ ] **Commit** : +```bash +git add server/handlers/agents.go server/main.go server/docker-compose.yml +git commit -m "feat(server): GET /api/agents/{id} + service iperf3 dans compose" +``` + +--- + +## Task 7 — Dashboard : section réseau dans popup détail + +**Files:** +- Modify: `dashboard/js/popups.js` +- Modify: `dashboard/css/app.css` + +- [ ] **Ajouter CSS** dans `app.css` pour la section réseau : + +```css +.net-table{display:flex;flex-direction:column;gap:4px} +.net-row{display:grid;grid-template-columns:auto 1fr 80px 120px 60px 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} +.net-row:first-child{background:var(--bg-4);font-size:9px;color:var(--ink-4);letter-spacing:.06em} +.net-val{font-family:var(--font-mono);font-size:11px;color:var(--ink-2)} +.hw-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px} +``` + +- [ ] **Dans `popups.js`**, après la section STOCKAGE dans `pop-body`, ajouter les sections réseau et hardware. Construire les variables HTML : + +```javascript +const netSection = entry?.agent?.network_info?.length > 0 + ? /* tableau des interfaces */ ... + : ''; + +const hwSection = entry?.agent?.hardware_info + ? /* grille hardware */ ... + : ''; +``` + +Insérer `${netSection}${hwSection}` avant la section INFORMATIONS. + +- [ ] **Commit** : +```bash +git add dashboard/js/popups.js dashboard/css/app.css +git commit -m "feat(dashboard): sections réseau et hardware dans popup détail" +``` + +--- + +## Task 8 — Dashboard : font-size global + +**Files:** +- Modify: `dashboard/js/app.js` +- Modify: `dashboard/css/app.css` + +- [ ] **Dans `app.js`**, changer l'application du font-size : appliquer sur `html` (root) au lieu de `body` : + +```javascript +if (_serverConfig.font_size) { + document.documentElement.style.fontSize = _serverConfig.font_size + 'px'; +} +``` + +- [ ] **Dans `app.css`**, vérifier que les éléments clés utilisent `rem` pour les tailles de police principales. Ajouter la règle de base sur `html` : + +```css +html { font-size: 13px; } /* valeur par défaut, écrasée par JS */ +``` + +Les éléments qui utilisent déjà des tailles en `px` absolues seront progressivement mis à l'échelle via ce mécanisme. Ceux qui héritent (`font-size: inherit`) bénéficieront automatiquement. + +- [ ] **Commit** : +```bash +git add dashboard/js/app.js dashboard/css/app.css +git commit -m "fix(dashboard): font-size global appliqué sur html root" +``` + +--- + +## Task 9 — Release et déploiement + +- [ ] **Rebuild agent** : `cargo build --release --manifest-path agent/Cargo.toml` +- [ ] **Copier binaires** dans `dist/` +- [ ] **Rebuild Docker** : `cd server && docker compose up -d --build` +- [ ] **Redéployer l'agent** via `install.sh` sur chaque VM cible +- [ ] **Push final** : `git push` diff --git a/server/main.go b/server/main.go index 9b626a4..d4c00e1 100644 --- a/server/main.go +++ b/server/main.go @@ -20,7 +20,7 @@ import ( ws "github.com/user/nanometrics/server/websocket" ) -const serverVersion = "0.1.0" +const serverVersion = "0.1.1" func main() { cfg := config.Load() diff --git a/server/transport/udp.go b/server/transport/udp.go index d48fc4f..817d2b7 100644 --- a/server/transport/udp.go +++ b/server/transport/udp.go @@ -2,6 +2,7 @@ package transport import ( "encoding/json" + "fmt" "log" "net" @@ -17,23 +18,31 @@ func StartUDP(addr string, handler func(*models.AgentMetrics)) error { go func() { buf := make([]byte, 65535) for { - n, _, err := conn.ReadFrom(buf) + n, src, err := conn.ReadFrom(buf) if err != nil { log.Printf("[udp] erreur lecture: %v", err) continue } data := make([]byte, n) copy(data, buf[:n]) - go processUDP(data, handler) + go processUDP(data, src.String(), handler) } }() return nil } -func processUDP(data []byte, handler func(*models.AgentMetrics)) { +func processUDP(data []byte, src string, handler func(*models.AgentMetrics)) { var m models.AgentMetrics if err := json.Unmarshal(data, &m); err != nil { - log.Printf("[udp] JSON invalide: %v", err) + preview := "" + if len(data) > 0 { + end := len(data) + if end > 32 { + end = 32 + } + preview = fmt.Sprintf(" | src=%s | premiers octets: %x | texte: %q", src, data[:end], data[:end]) + } + log.Printf("[udp] JSON invalide: %v%s", err, preview) return } if m.Hostname == "" {