Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1e1aa40d8 | |||
| 7fb47ffde8 | |||
| 3c15943e2e | |||
| a9506a5505 | |||
| ee5e8710a3 | |||
| d715b452c1 | |||
| fdeb4c2088 | |||
| 66605e22e3 | |||
| 1250cd7d3c | |||
| dc60fe2a8d | |||
| 55e68189d3 | |||
| db6fc65ee1 | |||
| 1002a6be68 |
@@ -410,7 +410,7 @@ body { background:var(--bg-1); color:var(--ink-1); font-family:var(--font-ui); f
|
|||||||
<div class="chk-box" id="chk-udp"><i class="fa-solid fa-check"></i></div>
|
<div class="chk-box" id="chk-udp"><i class="fa-solid fa-check"></i></div>
|
||||||
<div class="chk-label">
|
<div class="chk-label">
|
||||||
<div class="chk-name">UDP <span class="chk-badge udp">UDP</span></div>
|
<div class="chk-name">UDP <span class="chk-badge udp">UDP</span></div>
|
||||||
<div class="chk-desc">Fire-and-forget · serveur 10.0.0.50 · port 9999</div>
|
<div class="chk-desc">Fire-and-forget · serveur 10.0.0.82 · port 9999</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-s
|
|||||||
<div class="cfg-body">
|
<div class="cfg-body">
|
||||||
<div class="cfg-section">
|
<div class="cfg-section">
|
||||||
<div class="cfg-sec-title">PROTOCOLES DE TRANSPORT</div>
|
<div class="cfg-sec-title">PROTOCOLES DE TRANSPORT</div>
|
||||||
<div class="check-row active"><div class="chk-box"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">UDP <span class="chk-badge udp">UDP</span></div><div class="chk-desc">Fire-and-forget · serveur 10.0.0.50 · port 9999</div></div></div>
|
<div class="check-row active"><div class="chk-box"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">UDP <span class="chk-badge udp">UDP</span></div><div class="chk-desc">Fire-and-forget · serveur 10.0.0.82 · port 9999</div></div></div>
|
||||||
<div class="check-row mqtt-active"><div class="chk-box" style="background:var(--purple);border-color:var(--purple);color:var(--bg-0)"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">MQTT <span class="chk-badge mqtt">MQTT</span></div><div class="chk-desc">Bidirectionnel · broker 10.0.0.3 · port 1883</div></div></div>
|
<div class="check-row mqtt-active"><div class="chk-box" style="background:var(--purple);border-color:var(--purple);color:var(--bg-0)"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">MQTT <span class="chk-badge mqtt">MQTT</span></div><div class="chk-desc">Bidirectionnel · broker 10.0.0.3 · port 1883</div></div></div>
|
||||||
<div class="mqtt-opts">
|
<div class="mqtt-opts">
|
||||||
<div class="mqtt-field"><label>Broker</label><input class="mqtt-input" type="text" value="10.0.0.3"></div>
|
<div class="mqtt-field"><label>Broker</label><input class="mqtt-input" type="text" value="10.0.0.3"></div>
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-s
|
|||||||
<!-- PROTOCOLES -->
|
<!-- PROTOCOLES -->
|
||||||
<div class="cfg-section">
|
<div class="cfg-section">
|
||||||
<div class="cfg-sec-title">PROTOCOLES DE TRANSPORT</div>
|
<div class="cfg-sec-title">PROTOCOLES DE TRANSPORT</div>
|
||||||
<div class="check-row active"><div class="chk-box"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">UDP <span class="chk-badge udp">UDP</span></div><div class="chk-desc">Fire-and-forget · 10.0.0.50:9999</div></div></div>
|
<div class="check-row active"><div class="chk-box"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">UDP <span class="chk-badge udp">UDP</span></div><div class="chk-desc">Fire-and-forget · 10.0.0.82:9999</div></div></div>
|
||||||
<div class="check-row mqtt-active"><div class="chk-box" style="background:var(--purple);border-color:var(--purple);color:var(--bg-0)"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">MQTT <span class="chk-badge mqtt">MQTT</span></div><div class="chk-desc">Bidirectionnel · 10.0.0.3:1883</div></div></div>
|
<div class="check-row mqtt-active"><div class="chk-box" style="background:var(--purple);border-color:var(--purple);color:var(--bg-0)"><i class="fa-solid fa-check"></i></div><div class="chk-label"><div class="chk-name">MQTT <span class="chk-badge mqtt">MQTT</span></div><div class="chk-desc">Bidirectionnel · 10.0.0.3:1883</div></div></div>
|
||||||
<div class="mqtt-opts">
|
<div class="mqtt-opts">
|
||||||
<div class="mqtt-field"><label>Broker</label><input class="mqtt-input" type="text" value="10.0.0.3"></div>
|
<div class="mqtt-field"><label>Broker</label><input class="mqtt-input" type="text" value="10.0.0.3"></div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{"type":"server-started","port":55731,"host":"0.0.0.0","url_host":"10.0.0.50","url":"http://10.0.0.50:55731","screen_dir":"/home/gilles/projects/nano_metrics/.superpowers/brainstorm/599687-1779425985/content","state_dir":"/home/gilles/projects/nano_metrics/.superpowers/brainstorm/599687-1779425985/state"}
|
{"type":"server-started","port":55731,"host":"0.0.0.0","url_host":"10.0.0.82","url":"http://10.0.0.82:55731","screen_dir":"/home/gilles/projects/nano_metrics/.superpowers/brainstorm/599687-1779425985/content","state_dir":"/home/gilles/projects/nano_metrics/.superpowers/brainstorm/599687-1779425985/state"}
|
||||||
{"type":"screen-added","file":"/home/gilles/projects/nano_metrics/.superpowers/brainstorm/599687-1779425985/content/approaches.html"}
|
{"type":"screen-added","file":"/home/gilles/projects/nano_metrics/.superpowers/brainstorm/599687-1779425985/content/approaches.html"}
|
||||||
{"source":"user-event","type":"click","text":"C\n \n SQLite + WebSocket ⭐ Recommandé\n SQLite — simplicité opérationnelle, suffisant pour 20+ agents avec rétention configurable.\n WebSocket — bidirectionnel dès le départ, sans surcoût opérationnel.\n \n AvantagesPas de conteneur DB supplémentaireWebSocket prêt pour extensions futuresSimple à debugger et sauvegarder\n LimitesRequêtes temporelles moins expressives qu'InfluxDBScalabilité limitée au-delà de ~100 agents","choice":"c","id":null,"timestamp":1779426035162}
|
{"source":"user-event","type":"click","text":"C\n \n SQLite + WebSocket ⭐ Recommandé\n SQLite — simplicité opérationnelle, suffisant pour 20+ agents avec rétention configurable.\n WebSocket — bidirectionnel dès le départ, sans surcoût opérationnel.\n \n AvantagesPas de conteneur DB supplémentaireWebSocket prêt pour extensions futuresSimple à debugger et sauvegarder\n LimitesRequêtes temporelles moins expressives qu'InfluxDBScalabilité limitée au-delà de ~100 agents","choice":"c","id":null,"timestamp":1779426035162}
|
||||||
{"source":"user-event","type":"click","text":"C\n \n SQLite + WebSocket ⭐ Recommandé\n SQLite — simplicité opérationnelle, suffisant pour 20+ agents avec rétention configurable.\n WebSocket — bidirectionnel dès le départ, sans surcoût opérationnel.\n \n AvantagesPas de conteneur DB supplémentaireWebSocket prêt pour extensions futuresSimple à debugger et sauvegarder\n LimitesRequêtes temporelles moins expressives qu'InfluxDBScalabilité limitée au-delà de ~100 agents","choice":"c","id":null,"timestamp":1779426056446}
|
{"source":"user-event","type":"click","text":"C\n \n SQLite + WebSocket ⭐ Recommandé\n SQLite — simplicité opérationnelle, suffisant pour 20+ agents avec rétention configurable.\n WebSocket — bidirectionnel dès le départ, sans surcoût opérationnel.\n \n AvantagesPas de conteneur DB supplémentaireWebSocket prêt pour extensions futuresSimple à debugger et sauvegarder\n LimitesRequêtes temporelles moins expressives qu'InfluxDBScalabilité limitée au-delà de ~100 agents","choice":"c","id":null,"timestamp":1779426056446}
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ Ligne de Conduite 1 : L'Agent de Télémétrie (Rust)
|
|||||||
|
|
||||||
Orchestration Temporelle : N'inclus aucun moteur asynchrone (comme Tokio). Les fréquences d'actualisation différenciées (ex: CPU toutes les 2s, Disque toutes les 60s) doivent être gérées via une boucle mono-thread utilisant des pauses natives std::thread::sleep pour suspendre complètement le processus.
|
Orchestration Temporelle : N'inclus aucun moteur asynchrone (comme Tokio). Les fréquences d'actualisation différenciées (ex: CPU toutes les 2s, Disque toutes les 60s) doivent être gérées via une boucle mono-thread utilisant des pauses natives std::thread::sleep pour suspendre complètement le processus.
|
||||||
|
|
||||||
Configuration : Implémente la lecture d'un fichier config.toml externe via la bibliothèque serde pour paramétrer dynamiquement l'adresse IP du serveur cible (10.0.0.50) et les métriques à activer.
|
Configuration : Implémente la lecture d'un fichier config.toml externe via la bibliothèque serde pour paramétrer dynamiquement l'adresse IP du serveur cible (10.0.0.82) et les métriques à activer.
|
||||||
|
|
||||||
Transport : Utilise le protocole UDP pour expédier les charges utiles (payloads) en JSON, privilégiant la vitesse sans état (modèle fire-and-forget) sur un réseau local.
|
Transport : Utilise le protocole UDP pour expédier les charges utiles (payloads) en JSON, privilégiant la vitesse sans état (modèle fire-and-forget) sur un réseau local.
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Créer `/etc/nanometrics/config.toml` :
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
ip = "10.0.0.50" # IP du serveur Go
|
ip = "10.0.0.82" # IP du serveur Go
|
||||||
port = 9999 # Port UDP du serveur
|
port = 9999 # Port UDP du serveur
|
||||||
|
|
||||||
[mqtt]
|
[mqtt]
|
||||||
|
|||||||
Generated
+1
-1
@@ -248,7 +248,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nanometrics-agent"
|
name = "nanometrics-agent"
|
||||||
version = "0.1.6"
|
version = "0.1.17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rumqttc",
|
"rumqttc",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nanometrics-agent"
|
name = "nanometrics-agent"
|
||||||
version = "0.1.8"
|
version = "0.1.17"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[server]
|
[server]
|
||||||
ip = "10.0.0.50"
|
ip = "10.0.0.82"
|
||||||
port = 9999
|
port = 9999
|
||||||
|
|
||||||
[protocols.udp]
|
[protocols.udp]
|
||||||
|
|||||||
+47
-3
@@ -10,22 +10,66 @@ extern "C" fn handle_signal(_: libc::c_int) {
|
|||||||
RUNNING.store(false, Ordering::Relaxed);
|
RUNNING.store(false, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn physical_ipv4_addrs() -> Vec<String> {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
unsafe {
|
||||||
|
let mut ifap = std::ptr::null_mut::<libc::ifaddrs>();
|
||||||
|
if libc::getifaddrs(&mut ifap) != 0 { return result; }
|
||||||
|
let mut ifa = ifap;
|
||||||
|
while !ifa.is_null() {
|
||||||
|
let flags = (*ifa).ifa_flags as i32;
|
||||||
|
let up = flags & libc::IFF_UP as i32 != 0;
|
||||||
|
let loopback = flags & libc::IFF_LOOPBACK as i32 != 0;
|
||||||
|
let pointop = flags & libc::IFF_POINTOPOINT as i32 != 0;
|
||||||
|
if !up || loopback || pointop { ifa = (*ifa).ifa_next; continue; }
|
||||||
|
|
||||||
|
let name = std::ffi::CStr::from_ptr((*ifa).ifa_name)
|
||||||
|
.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
// Type 1 = ARPHRD_ETHER (Ethernet + WiFi), exclut WireGuard (65534), tunnels, etc.
|
||||||
|
let itype: u32 = std::fs::read_to_string(
|
||||||
|
format!("/sys/class/net/{}/type", name))
|
||||||
|
.ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
|
||||||
|
if itype != 1 { ifa = (*ifa).ifa_next; continue; }
|
||||||
|
|
||||||
|
// Exclut bridges et interfaces Docker par nom
|
||||||
|
let is_virtual = name.starts_with("br-") || name.starts_with("docker")
|
||||||
|
|| name.starts_with("virbr") || name.starts_with("veth");
|
||||||
|
if is_virtual { ifa = (*ifa).ifa_next; continue; }
|
||||||
|
|
||||||
|
if let Some(addr) = (*ifa).ifa_addr.as_ref() {
|
||||||
|
if addr.sa_family as i32 == libc::AF_INET {
|
||||||
|
let sin = addr as *const _ as *const libc::sockaddr_in;
|
||||||
|
let b = (*sin).sin_addr.s_addr.to_ne_bytes();
|
||||||
|
result.push(format!("{}.{}.{}.{}", b[0], b[1], b[2], b[3]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ifa = (*ifa).ifa_next;
|
||||||
|
}
|
||||||
|
libc::freeifaddrs(ifap);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
fn get_local_ip(server_ip: &str) -> String {
|
fn get_local_ip(server_ip: &str) -> String {
|
||||||
|
let physical = physical_ipv4_addrs();
|
||||||
|
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
// Try server IP first (always reachable), then internet fallback
|
|
||||||
for target in &[format!("{}:80", server_ip), "8.8.8.8:80".to_string()] {
|
for target in &[format!("{}:80", server_ip), "8.8.8.8:80".to_string()] {
|
||||||
if let Ok(s) = UdpSocket::bind("0.0.0.0:0") {
|
if let Ok(s) = UdpSocket::bind("0.0.0.0:0") {
|
||||||
if s.connect(target.as_str()).is_ok() {
|
if s.connect(target.as_str()).is_ok() {
|
||||||
if let Ok(addr) = s.local_addr() {
|
if let Ok(addr) = s.local_addr() {
|
||||||
let ip = addr.ip().to_string();
|
let ip = addr.ip().to_string();
|
||||||
if ip != "0.0.0.0" {
|
// N'accepte que si c'est une vraie interface physique
|
||||||
|
if ip != "0.0.0.0" && physical.contains(&ip) {
|
||||||
return ip;
|
return ip;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"0.0.0.0".to_string()
|
// Fallback : première IP physique disponible
|
||||||
|
physical.into_iter().next().unwrap_or_else(|| "0.0.0.0".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_config_update(cfg: &mut config::Config, data: &[u8]) {
|
fn apply_config_update(cfg: &mut config::Config, data: &[u8]) {
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ pub fn current_yday() -> u32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_physical(name: &str) -> bool {
|
fn is_physical(name: &str) -> bool {
|
||||||
if name == "lo" { return false; }
|
// Type 1 = ARPHRD_ETHER (Ethernet + WiFi). WireGuard = 65534, tunnels = autres.
|
||||||
for prefix in &["veth", "docker", "br-", "virbr", "vir", "tun", "tap", "dummy", "bond"] {
|
let itype: u32 = std::fs::read_to_string(format!("/sys/class/net/{}/type", name))
|
||||||
if name.starts_with(prefix) { return false; }
|
.ok().and_then(|s| s.trim().parse().ok()).unwrap_or(0);
|
||||||
}
|
if itype != 1 { return false; }
|
||||||
true
|
// Exclut bridges et interfaces Docker par nom (type 1 aussi)
|
||||||
|
!name.starts_with("br-") && !name.starts_with("docker")
|
||||||
|
&& !name.starts_with("virbr") && !name.starts_with("veth")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_sysfs(iface: &str, file: &str) -> Option<String> {
|
fn read_sysfs(iface: &str, file: &str) -> Option<String> {
|
||||||
|
|||||||
+52
-34
@@ -2,17 +2,24 @@ use serde::Deserialize;
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SmartJson {
|
struct SmartJson {
|
||||||
smart_status: SmartStatus,
|
#[serde(default)]
|
||||||
|
smart_status: Option<SmartStatus>,
|
||||||
temperature: Option<SmartTemp>,
|
temperature: Option<SmartTemp>,
|
||||||
ata_smart_attributes: Option<SmartAttrs>,
|
ata_smart_attributes: Option<SmartAttrs>,
|
||||||
nvme_smart_health_information_log: Option<NvmeHealth>,
|
nvme_smart_health_information_log: Option<NvmeHealth>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SmartStatus { passed: bool }
|
struct SmartStatus {
|
||||||
|
#[serde(default)]
|
||||||
|
passed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SmartTemp { current: i64 }
|
struct SmartTemp {
|
||||||
|
#[serde(default)]
|
||||||
|
current: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct SmartAttrs { table: Vec<SmartAttr> }
|
struct SmartAttrs { table: Vec<SmartAttr> }
|
||||||
@@ -44,7 +51,7 @@ pub fn is_available() -> bool {
|
|||||||
pub fn parse_json(json: &str) -> Result<crate::payload::SmartMetrics, serde_json::Error> {
|
pub fn parse_json(json: &str) -> Result<crate::payload::SmartMetrics, serde_json::Error> {
|
||||||
let s: SmartJson = serde_json::from_str(json)?;
|
let s: SmartJson = serde_json::from_str(json)?;
|
||||||
|
|
||||||
let temperature = s.temperature.as_ref().map(|t| t.current)
|
let temperature = s.temperature.as_ref().and_then(|t| t.current)
|
||||||
.or_else(|| s.nvme_smart_health_information_log.as_ref()?.temperature);
|
.or_else(|| s.nvme_smart_health_information_log.as_ref()?.temperature);
|
||||||
|
|
||||||
let mut reallocated = None;
|
let mut reallocated = None;
|
||||||
@@ -72,7 +79,7 @@ pub fn parse_json(json: &str) -> Result<crate::payload::SmartMetrics, serde_json
|
|||||||
|
|
||||||
Ok(crate::payload::SmartMetrics {
|
Ok(crate::payload::SmartMetrics {
|
||||||
device: String::new(),
|
device: String::new(),
|
||||||
passed: s.smart_status.passed,
|
passed: s.smart_status.as_ref().map(|s| s.passed).unwrap_or(false),
|
||||||
temperature,
|
temperature,
|
||||||
reallocated_sectors: reallocated,
|
reallocated_sectors: reallocated,
|
||||||
power_on_hours: power_hours,
|
power_on_hours: power_hours,
|
||||||
@@ -82,43 +89,54 @@ pub fn parse_json(json: &str) -> Result<crate::payload::SmartMetrics, serde_json
|
|||||||
|
|
||||||
pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
|
pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
|
||||||
if !is_available() {
|
if !is_available() {
|
||||||
|
eprintln!("[smart] smartctl introuvable dans PATH");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut devs: Vec<String> = std::fs::read_dir("/sys/block")
|
let mut set = std::collections::HashSet::new();
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
// SATA/SAS : /sys/block/sd* → /dev/sda, /dev/sdb…
|
||||||
.flatten()
|
for e in std::fs::read_dir("/sys/block").into_iter().flatten().flatten() {
|
||||||
.map(|e| e.file_name().into_string().unwrap_or_default())
|
let n = e.file_name().into_string().unwrap_or_default();
|
||||||
.filter_map(|n| {
|
if n.starts_with("sd") {
|
||||||
if n.starts_with("sd") {
|
set.insert(format!("/dev/{}", n));
|
||||||
Some(format!("/dev/{}", n))
|
}
|
||||||
} else if n.starts_with("nvme") && !n.contains('n') {
|
}
|
||||||
// /sys/block contient nvme0n1 (namespace) — on utilise le contrôleur nvme0
|
|
||||||
Some(format!("/dev/{}", n))
|
// NVMe : /sys/class/nvme/nvme* → /dev/nvme0, /dev/nvme1…
|
||||||
} else if n.starts_with("nvme") && n.contains('n') {
|
// On utilise le contrôleur (char device), pas le namespace (block device),
|
||||||
// Déduit le contrôleur depuis nvme0n1 → nvme0
|
// car smartctl ne peut exécuter les commandes admin SMART que via le contrôleur.
|
||||||
let ctrl = n.split('n').next()?;
|
// La règle udev 99-nanometrics-smart.rules lui donne l'accès groupe disk.
|
||||||
Some(format!("/dev/{}", ctrl))
|
for e in std::fs::read_dir("/sys/class/nvme").into_iter().flatten().flatten() {
|
||||||
} else {
|
let n = e.file_name().into_string().unwrap_or_default();
|
||||||
None
|
if n.starts_with("nvme") {
|
||||||
}
|
set.insert(format!("/dev/{}", n));
|
||||||
})
|
}
|
||||||
.collect::<std::collections::HashSet<_>>()
|
}
|
||||||
.into_iter()
|
|
||||||
.collect();
|
let mut devs: Vec<String> = set.into_iter().collect();
|
||||||
devs.sort();
|
devs.sort();
|
||||||
|
eprintln!("[smart] disques détectés: {:?}", devs);
|
||||||
|
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
for dev in &devs {
|
for dev in &devs {
|
||||||
let Ok(output) = std::process::Command::new("smartctl")
|
let output = match std::process::Command::new("smartctl")
|
||||||
.args(["-a", "-j", dev])
|
.args(["-a", "-j", dev])
|
||||||
.output() else { continue };
|
.output()
|
||||||
|
{
|
||||||
|
Ok(o) => o,
|
||||||
|
Err(e) => { eprintln!("[smart] erreur exec smartctl {}: {}", dev, e); continue }
|
||||||
|
};
|
||||||
let json = String::from_utf8_lossy(&output.stdout);
|
let json = String::from_utf8_lossy(&output.stdout);
|
||||||
if let Ok(metrics) = parse_json(&json) {
|
match parse_json(&json) {
|
||||||
results.push(crate::payload::SmartMetrics {
|
Ok(metrics) => {
|
||||||
device: dev.trim_start_matches("/dev/").to_string(),
|
results.push(crate::payload::SmartMetrics {
|
||||||
..metrics
|
device: dev.trim_start_matches("/dev/").to_string(),
|
||||||
});
|
..metrics
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[smart] {} parse JSON échoué: {}", dev, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if results.is_empty() { None } else { Some(results) }
|
if results.is_empty() { None } else { Some(results) }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ fn test_config_parse_complet() {
|
|||||||
let mut f = NamedTempFile::new().unwrap();
|
let mut f = NamedTempFile::new().unwrap();
|
||||||
write!(f, r#"
|
write!(f, r#"
|
||||||
[server]
|
[server]
|
||||||
ip = "10.0.0.50"
|
ip = "10.0.0.82"
|
||||||
port = 9999
|
port = 9999
|
||||||
|
|
||||||
[protocols.udp]
|
[protocols.udp]
|
||||||
@@ -26,7 +26,7 @@ udp = true
|
|||||||
mqtt = false
|
mqtt = false
|
||||||
"#).unwrap();
|
"#).unwrap();
|
||||||
let cfg = nanometrics_agent::config::load(f.path()).unwrap();
|
let cfg = nanometrics_agent::config::load(f.path()).unwrap();
|
||||||
assert_eq!(cfg.server.ip, "10.0.0.50");
|
assert_eq!(cfg.server.ip, "10.0.0.82");
|
||||||
assert_eq!(cfg.server.port, 9999);
|
assert_eq!(cfg.server.port, 9999);
|
||||||
assert!(cfg.protocols.udp.enabled);
|
assert!(cfg.protocols.udp.enabled);
|
||||||
assert!(cfg.protocols.mqtt.enabled);
|
assert!(cfg.protocols.mqtt.enabled);
|
||||||
@@ -40,7 +40,7 @@ fn test_config_mqtt_absent() {
|
|||||||
let mut f = NamedTempFile::new().unwrap();
|
let mut f = NamedTempFile::new().unwrap();
|
||||||
write!(f, r#"
|
write!(f, r#"
|
||||||
[server]
|
[server]
|
||||||
ip = "10.0.0.50"
|
ip = "10.0.0.82"
|
||||||
port = 9999
|
port = 9999
|
||||||
|
|
||||||
[protocols.udp]
|
[protocols.udp]
|
||||||
|
|||||||
@@ -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 ? │
|
||||||
|
│ │
|
||||||
|
│ <Oui> <Non> , peut on faire une installe silencieuse pour le script des agent en repondant non
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -15,7 +15,13 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'JetBrains Mono';
|
font-family: 'JetBrains Mono';
|
||||||
src: url('../fonts/jetbrains-mono.woff2') format('woff2');
|
src: url('../fonts/jetbrains-mono.woff2') format('woff2');
|
||||||
font-weight: 400 700;
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
src: url('../fonts/jetbrains-mono-bold.woff2') format('woff2');
|
||||||
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,6 +2,7 @@ const Popups = (() => {
|
|||||||
let _currentAgentId = null;
|
let _currentAgentId = null;
|
||||||
let _agentCfgData = null;
|
let _agentCfgData = null;
|
||||||
let _resizeObs = null;
|
let _resizeObs = null;
|
||||||
|
let _resizeTimer = null;
|
||||||
|
|
||||||
// ══ POPUP DÉTAIL ══
|
// ══ POPUP DÉTAIL ══
|
||||||
async function showDetail(agentId) {
|
async function showDetail(agentId) {
|
||||||
@@ -214,11 +215,14 @@ const Popups = (() => {
|
|||||||
if (_resizeObs) _resizeObs.disconnect();
|
if (_resizeObs) _resizeObs.disconnect();
|
||||||
const pd = document.getElementById('popup-detail');
|
const pd = document.getElementById('popup-detail');
|
||||||
_resizeObs = new ResizeObserver(() => {
|
_resizeObs = new ResizeObserver(() => {
|
||||||
API.putServerConfig({
|
clearTimeout(_resizeTimer);
|
||||||
...App.serverConfig,
|
_resizeTimer = setTimeout(() => {
|
||||||
popup_detail_w: pd.offsetWidth,
|
API.putServerConfig({
|
||||||
popup_detail_h: pd.offsetHeight,
|
...App.serverConfig,
|
||||||
}).catch(() => {});
|
popup_detail_w: pd.offsetWidth,
|
||||||
|
popup_detail_h: pd.offsetHeight,
|
||||||
|
}).catch(() => {});
|
||||||
|
}, 600);
|
||||||
});
|
});
|
||||||
_resizeObs.observe(pd);
|
_resizeObs.observe(pd);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Nanometrics: accès groupe disk aux contrôleurs NVMe pour SMART
|
||||||
|
# Sans cette règle, /dev/nvme0 est crw------- root root (root only),
|
||||||
|
# ce qui empêche smartctl d'exécuter les commandes admin et omet smart_status du JSON.
|
||||||
|
KERNEL=="nvme[0-9]*", SUBSYSTEM=="nvme", GROUP="disk", MODE="0660"
|
||||||
+13
-2
@@ -2,7 +2,7 @@
|
|||||||
# Installe l'agent Nanometrics depuis la dernière release Gitea.
|
# Installe l'agent Nanometrics depuis la dernière release Gitea.
|
||||||
# Usage :
|
# Usage :
|
||||||
# curl -fsSL https://git.maison43gil.com/gilles/nano_metrics/raw/branch/main/deploy/install.sh | bash
|
# curl -fsSL https://git.maison43gil.com/gilles/nano_metrics/raw/branch/main/deploy/install.sh | bash
|
||||||
# SERVER_IP=10.0.0.50 SERVER_PORT=9999 curl -fsSL ... | bash
|
# SERVER_IP=10.0.0.82 SERVER_PORT=9999 curl -fsSL ... | bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REPO_API="https://git.maison43gil.com/api/v1/repos/gilles/nano_metrics"
|
REPO_API="https://git.maison43gil.com/api/v1/repos/gilles/nano_metrics"
|
||||||
@@ -45,6 +45,17 @@ else
|
|||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# ── 2. Règle udev NVMe (accès SMART pour le groupe disk) ──────────────────────
|
||||||
|
UDEV_RULE="/etc/udev/rules.d/99-nanometrics-smart.rules"
|
||||||
|
cat > "$UDEV_RULE" << 'UDEVRULE'
|
||||||
|
# Nanometrics: accès groupe disk aux contrôleurs NVMe pour SMART
|
||||||
|
KERNEL=="nvme[0-9]*", SUBSYSTEM=="nvme", GROUP="disk", MODE="0660"
|
||||||
|
UDEVRULE
|
||||||
|
udevadm control --reload-rules
|
||||||
|
udevadm trigger --subsystem-match=nvme 2>/dev/null || true
|
||||||
|
ok "Règle udev NVMe installée ($UDEV_RULE)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
# ── 3. Détection de l'architecture ────────────────────────────────────────────
|
# ── 3. Détection de l'architecture ────────────────────────────────────────────
|
||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
@@ -98,7 +109,7 @@ ok "Binaire téléchargé ($(du -sh "$TMP_BIN" | cut -f1))"
|
|||||||
echo ""
|
echo ""
|
||||||
echo "--- Configuration du serveur ---"
|
echo "--- Configuration du serveur ---"
|
||||||
|
|
||||||
SERVER_IP="${SERVER_IP:-10.0.0.50}"
|
SERVER_IP="${SERVER_IP:-10.0.0.82}"
|
||||||
SERVER_PORT="${SERVER_PORT:-9999}"
|
SERVER_PORT="${SERVER_PORT:-9999}"
|
||||||
MQTT_HOST="${MQTT_HOST:-10.0.0.3}"
|
MQTT_HOST="${MQTT_HOST:-10.0.0.3}"
|
||||||
MQTT_ENABLED="${MQTT_ENABLED:-false}"
|
MQTT_ENABLED="${MQTT_ENABLED:-false}"
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ ConfigurationDirectoryMode=0755
|
|||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=read-only
|
ProtectHome=read-only
|
||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
NoNewPrivileges=yes
|
# CAP_SYS_ADMIN est requis par le noyau pour NVME_IOCTL_ADMIN_CMD (lecture SMART NVMe).
|
||||||
|
# NoNewPrivileges est retiré car il efface les ambient capabilities sur exec (noyau ≥ 5.2),
|
||||||
|
# ce qui empêcherait smartctl enfant d'hériter la capability.
|
||||||
|
# CapabilityBoundingSet borne à la seule cap nécessaire.
|
||||||
|
CapabilityBoundingSet=CAP_SYS_ADMIN
|
||||||
|
AmbientCapabilities=CAP_SYS_ADMIN
|
||||||
|
|
||||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||||
|
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -79,7 +79,7 @@ tempfile = "3"
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[server]
|
[server]
|
||||||
ip = "10.0.0.50"
|
ip = "10.0.0.82"
|
||||||
port = 9999
|
port = 9999
|
||||||
|
|
||||||
[protocols.udp]
|
[protocols.udp]
|
||||||
@@ -172,7 +172,7 @@ fn test_config_parse_complet() {
|
|||||||
let mut f = NamedTempFile::new().unwrap();
|
let mut f = NamedTempFile::new().unwrap();
|
||||||
write!(f, r#"
|
write!(f, r#"
|
||||||
[server]
|
[server]
|
||||||
ip = "10.0.0.50"
|
ip = "10.0.0.82"
|
||||||
port = 9999
|
port = 9999
|
||||||
|
|
||||||
[protocols.udp]
|
[protocols.udp]
|
||||||
@@ -192,7 +192,7 @@ udp = true
|
|||||||
mqtt = false
|
mqtt = false
|
||||||
"#).unwrap();
|
"#).unwrap();
|
||||||
let cfg = nanometrics_agent::config::load(f.path()).unwrap();
|
let cfg = nanometrics_agent::config::load(f.path()).unwrap();
|
||||||
assert_eq!(cfg.server.ip, "10.0.0.50");
|
assert_eq!(cfg.server.ip, "10.0.0.82");
|
||||||
assert_eq!(cfg.server.port, 9999);
|
assert_eq!(cfg.server.port, 9999);
|
||||||
assert!(cfg.protocols.udp.enabled);
|
assert!(cfg.protocols.udp.enabled);
|
||||||
assert!(cfg.protocols.mqtt.enabled);
|
assert!(cfg.protocols.mqtt.enabled);
|
||||||
@@ -206,7 +206,7 @@ fn test_config_mqtt_absent() {
|
|||||||
let mut f = NamedTempFile::new().unwrap();
|
let mut f = NamedTempFile::new().unwrap();
|
||||||
write!(f, r#"
|
write!(f, r#"
|
||||||
[server]
|
[server]
|
||||||
ip = "10.0.0.50"
|
ip = "10.0.0.82"
|
||||||
port = 9999
|
port = 9999
|
||||||
|
|
||||||
[protocols.udp]
|
[protocols.udp]
|
||||||
|
|||||||
@@ -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<i64>,
|
||||||
|
pub mac: String,
|
||||||
|
pub wol: Option<bool>,
|
||||||
|
pub iperf_mbps: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||||
|
pub struct HardwareInfo {
|
||||||
|
pub motherboard_vendor: Option<String>,
|
||||||
|
pub motherboard_model: Option<String>,
|
||||||
|
pub cpu_model: Option<String>,
|
||||||
|
pub ram_type: Option<String>,
|
||||||
|
pub ram_speed_mhz: Option<i64>,
|
||||||
|
pub ram_slots_used: Option<i64>,
|
||||||
|
pub ram_slots_total: Option<i64>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans `AgentMetrics`, ajouter après `smart` :
|
||||||
|
```rust
|
||||||
|
pub network_info: Option<Vec<NetworkInterface>>,
|
||||||
|
pub hardware_info: Option<HardwareInfo>,
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **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::<libc::tm>::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<String> {
|
||||||
|
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<bool> {
|
||||||
|
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<f64> {
|
||||||
|
// 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<crate::payload::NetworkInterface> {
|
||||||
|
let entries = match std::fs::read_dir("/sys/class/net") {
|
||||||
|
Ok(e) => e, Err(_) => return vec![],
|
||||||
|
};
|
||||||
|
let mut ifaces: Vec<String> = 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::<i64>().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<String> {
|
||||||
|
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<crate::payload::HardwareInfo> {
|
||||||
|
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<String> = None;
|
||||||
|
let mut ram_speed: Option<i64> = 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::<libc::tm>::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`
|
||||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
|||||||
ws "github.com/user/nanometrics/server/websocket"
|
ws "github.com/user/nanometrics/server/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
const serverVersion = "0.1.0"
|
const serverVersion = "0.1.1"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
|||||||
+14
-4
@@ -17,27 +17,37 @@ func StartUDP(addr string, handler func(*models.AgentMetrics)) error {
|
|||||||
go func() {
|
go func() {
|
||||||
buf := make([]byte, 65535)
|
buf := make([]byte, 65535)
|
||||||
for {
|
for {
|
||||||
n, _, err := conn.ReadFrom(buf)
|
n, src, err := conn.ReadFrom(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[udp] erreur lecture: %v", err)
|
log.Printf("[udp] erreur lecture: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data := make([]byte, n)
|
data := make([]byte, n)
|
||||||
copy(data, buf[:n])
|
copy(data, buf[:n])
|
||||||
go processUDP(data, handler)
|
go processUDP(data, src.String(), handler)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func processUDP(data []byte, handler func(*models.AgentMetrics)) {
|
func processUDP(data []byte, src string, handler func(*models.AgentMetrics)) {
|
||||||
var m models.AgentMetrics
|
var m models.AgentMetrics
|
||||||
if err := json.Unmarshal(data, &m); err != nil {
|
if err := json.Unmarshal(data, &m); err != nil {
|
||||||
log.Printf("[udp] JSON invalide: %v", err)
|
end := 32
|
||||||
|
if len(data) < end {
|
||||||
|
end = len(data)
|
||||||
|
}
|
||||||
|
log.Printf("[udp] JSON invalide: %v | src=%s | octets: %x", err, src, data[:end])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if m.Hostname == "" {
|
if m.Hostname == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// DEBUG SMART — logguer le payload ASUS complet
|
||||||
|
if m.Smart != nil {
|
||||||
|
log.Printf("[udp] SMART reçu de %s: %d disque(s)", m.Hostname, len(m.Smart))
|
||||||
|
} else {
|
||||||
|
log.Printf("[udp] payload de %s (v%s): smart=nil hdd=%v", m.Hostname, m.Version, m.HDDTotal)
|
||||||
|
}
|
||||||
handler(&m)
|
handler(&m)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user