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 0000000..9182ad5 Binary files /dev/null and b/capture/image1.png differ 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 == "" {