3 Commits

Author SHA1 Message Date
Gilles Soulier db6fc65ee1 fix(v0.1.9): détection IP/interface — filtre VPN WireGuard par flags kernel
- get_local_ip: construit d'abord la liste des IPs physiques (getifaddrs
  avec IFF_POINTOPOINT exclu + type=1 ARPHRD_ETHER requis), puis vérifie
  que l'IP choisie par le UDP-connect-trick en fait partie → évite de
  retourner une IP VPN même quand le trafic y est routé
- is_physical: remplace le filtrage par préfixe de nom par type kernel
  /sys/class/net/<iface>/type == 1 (Ethernet/WiFi) ; exclut WireGuard
  (type 65534), tunnels et autres interfaces virtuelles nommées librement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 07:28:14 +02:00
Gilles Soulier 1002a6be68 fix: polices woff2 invalides + debounce ResizeObserver config
- jetbrains-mono.woff2 et share-tech-mono.woff2 étaient des fichiers HTML
  (pages 404 téléchargées par erreur) → remplacés par les vrais binaires wOF2
- JetBrains Mono : fichiers séparés regular/bold (400 et 700)
- ResizeObserver popup détail : debounce 600ms pour éviter 50+ PUT /api/config
  lors d'un resize

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 07:14:11 +02:00
Gilles Soulier 017d7bb1bb fix(smart v0.1.8): NVMe — contrôleur correct + flag -a pour attributs complets
- /sys/block expose nvme0n1 (namespace), mais smartctl a besoin du contrôleur
  nvme0 → déduplication via HashSet pour éviter les doublons nvme0n1/nvme0
- smartctl -j → smartctl -a -j pour inclure nvme_smart_health_information_log
  (sans -a, le log de santé NVMe n'est pas dans la sortie JSON)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 07:07:45 +02:00
11 changed files with 88 additions and 18 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nanometrics-agent"
version = "0.1.7"
version = "0.1.9"
edition = "2021"
[lib]
+47 -3
View File
@@ -10,22 +10,66 @@ extern "C" fn handle_signal(_: libc::c_int) {
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 {
let physical = physical_ipv4_addrs();
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()] {
if let Ok(s) = UdpSocket::bind("0.0.0.0:0") {
if s.connect(target.as_str()).is_ok() {
if let Ok(addr) = s.local_addr() {
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;
}
}
}
}
}
"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]) {
+7 -5
View File
@@ -26,11 +26,13 @@ pub fn current_yday() -> u32 {
}
fn is_physical(name: &str) -> bool {
if name == "lo" { return false; }
for prefix in &["veth", "docker", "br-", "virbr", "vir", "tun", "tap", "dummy", "bond"] {
if name.starts_with(prefix) { return false; }
}
true
// Type 1 = ARPHRD_ETHER (Ethernet + WiFi). WireGuard = 65534, tunnels = autres.
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 { return false; }
// 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> {
+17 -3
View File
@@ -89,15 +89,29 @@ pub fn collect() -> Option<Vec<crate::payload::SmartMetrics>> {
.flatten()
.flatten()
.map(|e| e.file_name().into_string().unwrap_or_default())
.filter(|n| n.starts_with("sd") || n.starts_with("nvme"))
.map(|n| format!("/dev/{}", n))
.filter_map(|n| {
if n.starts_with("sd") {
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))
} else if n.starts_with("nvme") && n.contains('n') {
// Déduit le contrôleur depuis nvme0n1 → nvme0
let ctrl = n.split('n').next()?;
Some(format!("/dev/{}", ctrl))
} else {
None
}
})
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
devs.sort();
let mut results = Vec::new();
for dev in &devs {
let Ok(output) = std::process::Command::new("smartctl")
.args(["-j", dev])
.args(["-a", "-j", dev])
.output() else { continue };
let json = String::from_utf8_lossy(&output.stdout);
if let Ok(metrics) = parse_json(&json) {
+7 -1
View File
@@ -15,7 +15,13 @@
@font-face {
font-family: 'JetBrains Mono';
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-face {
Binary file not shown.
Binary file not shown.
Binary file not shown.
+9 -5
View File
@@ -2,6 +2,7 @@ const Popups = (() => {
let _currentAgentId = null;
let _agentCfgData = null;
let _resizeObs = null;
let _resizeTimer = null;
// ══ POPUP DÉTAIL ══
async function showDetail(agentId) {
@@ -214,11 +215,14 @@ const Popups = (() => {
if (_resizeObs) _resizeObs.disconnect();
const pd = document.getElementById('popup-detail');
_resizeObs = new ResizeObserver(() => {
API.putServerConfig({
...App.serverConfig,
popup_detail_w: pd.offsetWidth,
popup_detail_h: pd.offsetHeight,
}).catch(() => {});
clearTimeout(_resizeTimer);
_resizeTimer = setTimeout(() => {
API.putServerConfig({
...App.serverConfig,
popup_detail_w: pd.offsetWidth,
popup_detail_h: pd.offsetHeight,
}).catch(() => {});
}, 600);
});
_resizeObs.observe(pd);
Binary file not shown.
Binary file not shown.