9 Commits

Author SHA1 Message Date
Gilles Soulier 243c97d71b fix: disque via statvfs() — valeurs identiques à df
Remplace sysinfo::Disks par un appel direct à libc::statvfs("/").
- used  = (f_blocks − f_bfree) × f_frsize  → correspond à df "Utilisé"
- free  = f_bavail × f_frsize              → correspond à df "Dispo"
- total = f_blocks × f_frsize

Avant (sysinfo) : used comptait les blocs réservés root → surestimation de ~3-4 Go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:21:58 +02:00
Gilles Soulier e0ed96309c fix: conserver les métriques lentes (disque, smart) entre les paquets
Le disque est envoyé toutes les 60s mais les paquets arrivent toutes les 2s.
Chaque nouveau paquet écrasait les champs null, effaçant le disque affiché.
Correction : fusion avec les anciennes métriques, null ne remplace pas une valeur.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:14:45 +02:00
Gilles Soulier 93747e4a04 feat: favicons + correctifs tuile (RAM overflow, corbeille droite)
Favicons :
- favicon.svg (scalable, navigateurs modernes)
- favicon.ico (16/32/48px, compatibilité universelle)
- favicon-{16,32,48,96,180,192,512}.png
- favicon-180.png pour apple-touch-icon
- site.webmanifest pour PWA / ajout écran d'accueil Android
- Couleurs Gruvbox : fond #282828, accent orange, LED verte

Tuile :
- g-val : min-width + white-space:nowrap (RAM 3.0Go/5.8Go ne déborde plus)
- tile-foot : justify-content:space-between + tile-foot-info wrapper
  (corbeille alignée en bas à droite)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:07:40 +02:00
Gilles Soulier b93b55d5a8 fix: RAM déborde de la tuile, corbeille alignée à droite
- g-val : largeur fixe 34px → min-width + white-space:nowrap (RAM "3.0Go/5.8Go")
- tile-foot : justify-content:space-between + wrapper tile-foot-info
  pour que la corbeille soit toujours en bas à droite

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:06:21 +02:00
Gilles Soulier 46209b2965 fix: chmod 644 sur config.toml (DynamicUser ne peut pas lire 640)
Avec DynamicUser=yes, le fichier config.toml créé en root:root 640
n'est pas lisible par l'utilisateur dynamique → exit 101 (panic Rust).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:58:02 +02:00
Gilles Soulier 775d54f07c feat: suppression agent, RAM en Go, métriques par défaut (cpu/mem/disk/smart)
- API DELETE /api/agents/{id} — supprime agent + métriques + config + icône
- Bouton poubelle sur chaque tuile + dialog de confirmation
- RAM : affichage "utilisé/total" en Go (ex: 6.2Go/8.0Go) au lieu du %
- Config agent par défaut : cpu, memory, disk, smart activés (UDP)
- DefaultAgentConfig() dans models pour les nouveaux agents

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:54:10 +02:00
Gilles Soulier e9524858f5 feat: commande d'installation agent dans la config serveur
Nouvelle section "INSTALLATION AGENT" en bas du popup de configuration :
champ lecture seule avec la commande curl pré-remplie (SERVER_IP auto
depuis window.location.hostname) + bouton Copier.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:46:52 +02:00
Gilles Soulier 1a1202abcf fix: valeurs par défaut install.sh (serveur 10.0.0.50, MQTT 10.0.0.3)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:42:39 +02:00
Gilles Soulier c526a6e5ca fix: cross-compilation musl pour release multi-arch
- rumqttc : use-native-tls → use-rustls (supprime dépendance OpenSSL)
- .cargo/config.toml (racine) : linkers musl + CC/AR pour ring aarch64
- deploy/release.sh : passe CC_aarch64_unknown_linux_musl au build
- .gitignore : règle config.toml affinée (exclut cargo configs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 19:42:08 +02:00
29 changed files with 365 additions and 144 deletions
+9
View File
@@ -0,0 +1,9 @@
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-gnu-gcc"
[env]
CC_aarch64_unknown_linux_musl = "aarch64-linux-gnu-gcc"
AR_aarch64_unknown_linux_musl = "aarch64-linux-gnu-ar"
+7 -1
View File
@@ -10,4 +10,10 @@ server/*.test
# OS
.DS_Store
Thumbs.db
config.toml
# Config locaux (contiennent des IPs/secrets)
agent/config.toml
server/config.toml
# Sauf les configs cargo (pas de secrets)
!.cargo/config.toml
!agent/.cargo/config.toml
+9
View File
@@ -0,0 +1,9 @@
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
[target.aarch64-unknown-linux-musl]
linker = "aarch64-linux-gnu-gcc"
[env]
CC_aarch64_unknown_linux_musl = "aarch64-linux-gnu-gcc"
AR_aarch64_unknown_linux_musl = "aarch64-linux-gnu-ar"
+129 -98
View File
@@ -38,9 +38,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "core-foundation"
version = "0.10.1"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
@@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -97,21 +97,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "futures-core"
version = "0.3.32"
@@ -142,6 +127,17 @@ dependencies = [
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.2"
@@ -247,13 +243,14 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "nanometrics-agent"
version = "0.1.0"
dependencies = [
"libc",
"rumqttc",
"serde",
"serde_json",
@@ -262,23 +259,6 @@ dependencies = [
"toml",
]
[[package]]
name = "native-tls"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "ntapi"
version = "0.4.3"
@@ -294,48 +274,11 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "openssl"
version = "0.10.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.2.1"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
version = "0.9.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "pin-project-lite"
@@ -343,12 +286,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "prettyplease"
version = "0.2.37"
@@ -383,6 +320,20 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rumqttc"
version = "0.24.0"
@@ -393,10 +344,12 @@ dependencies = [
"flume",
"futures-util",
"log",
"native-tls",
"rustls-native-certs",
"rustls-pemfile",
"rustls-webpki",
"thiserror",
"tokio",
"tokio-native-tls",
"tokio-rustls",
]
[[package]]
@@ -409,7 +362,63 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
dependencies = [
"log",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
@@ -418,7 +427,7 @@ version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -429,9 +438,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation",
@@ -527,7 +536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -539,6 +548,12 @@ dependencies = [
"lock_api",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.117"
@@ -571,10 +586,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -609,7 +624,7 @@ dependencies = [
"pin-project-lite",
"socket2",
"tokio-macros",
"windows-sys",
"windows-sys 0.61.2",
]
[[package]]
@@ -624,12 +639,13 @@ dependencies = [
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
name = "tokio-rustls"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [
"native-tls",
"rustls",
"rustls-pki-types",
"tokio",
]
@@ -687,10 +703,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "vcpkg"
version = "0.2.15"
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "wasi"
@@ -797,6 +813,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -973,6 +998,12 @@ dependencies = [
"wasmparser",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zmij"
version = "1.0.21"
+2 -1
View File
@@ -19,10 +19,11 @@ codegen-units = 1
[dependencies]
sysinfo = { version = "0.30", default-features = false }
libc = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
rumqttc = { version = "0.24", default-features = false, features = ["use-native-tls"] }
rumqttc = { version = "0.24", default-features = false, features = ["use-rustls"] }
[dev-dependencies]
tempfile = "3"
+2 -4
View File
@@ -1,5 +1,5 @@
use nanometrics_agent::{config, metrics, payload, transport};
use sysinfo::{Components, Disks, Networks, System};
use sysinfo::{Components, Networks, System};
use std::time::{Duration, Instant};
use std::sync::mpsc;
@@ -34,7 +34,6 @@ fn main() {
let mut sys = System::new();
let mut networks = Networks::new_with_refreshed_list();
let mut disks = Disks::new_with_refreshed_list();
let mut components = Components::new_with_refreshed_list();
let udp_sender = if cfg.protocols.udp.enabled {
@@ -102,9 +101,8 @@ fn main() {
}
if first_slow || now.duration_since(last_slow).as_secs() >= 60 {
disks.refresh();
if cfg.metrics.disk.udp || cfg.metrics.disk.mqtt {
let (used, free, total) = metrics::disk::get(&disks);
let (used, free, total) = metrics::disk::get();
m.hdd_used = Some(used);
m.hdd_free = Some(free);
m.hdd_total = Some(total);
+22 -20
View File
@@ -1,34 +1,36 @@
use sysinfo::Disks;
use std::mem::MaybeUninit;
pub fn get(disks: &Disks) -> (u64, u64, u64) {
for disk in disks.list() {
let mount = disk.mount_point().to_string_lossy();
if mount == "/" {
let total = disk.total_space();
let free = disk.available_space();
let used = total.saturating_sub(free);
return (used, free, total);
}
/// Retourne (used, free, total) en octets pour le système de fichiers racine "/".
/// Utilise statvfs() directement pour correspondre exactement aux chiffres de `df` :
/// - total = f_blocks × f_frsize
/// - used = (f_blocks f_bfree) × f_frsize (blocs effectivement écrits)
/// - free = f_bavail × f_frsize (disponible pour utilisateurs non-root)
pub fn get() -> (u64, u64, u64) {
let path = b"/\0";
let mut stat = MaybeUninit::<libc::statvfs>::uninit();
let ret = unsafe { libc::statvfs(path.as_ptr() as *const libc::c_char, stat.as_mut_ptr()) };
if ret != 0 {
return (0, 0, 0);
}
if let Some(disk) = disks.list().first() {
let total = disk.total_space();
let free = disk.available_space();
return (total.saturating_sub(free), free, total);
}
(0, 0, 0)
let stat = unsafe { stat.assume_init() };
let bsize = stat.f_frsize as u64;
let total = stat.f_blocks.saturating_mul(bsize);
let used = stat.f_blocks.saturating_sub(stat.f_bfree).saturating_mul(bsize);
let free = stat.f_bavail.saturating_mul(bsize);
(used, free, total)
}
#[cfg(test)]
mod tests {
use super::*;
use sysinfo::Disks;
#[test]
fn test_disk_coherent() {
let disks = Disks::new_with_refreshed_list();
let (used, free, total) = get(&disks);
let (used, free, total) = get();
eprintln!("résultat statvfs : used={used} free={free} total={total}");
if total > 0 {
assert!(used + free <= total + 1024, "used + free > total");
assert!(used + free <= total + 1024 * 1024, "used + free > total");
assert!(total > 0, "total doit être > 0");
}
}
}
+8 -2
View File
@@ -115,9 +115,15 @@ body{background:var(--bg-1);color:var(--ink-1);font-family:var(--font-ui);font-s
.g-bar{flex:1;height:5px;border-radius:3px;background:var(--bg-1);overflow:hidden}
.g-fill{height:100%;border-radius:3px;background:var(--ok);transition:width .3s}
.g-fill.w{background:var(--warn)}.g-fill.e{background:var(--err)}
.g-val{font-family:var(--font-mono);font-size:11px;color:var(--ink-2);width:34px;text-align:right}
.g-val{font-family:var(--font-mono);font-size:11px;color:var(--ink-2);
min-width:34px;white-space:nowrap;flex-shrink:0;text-align:right}
.tile-foot{font-family:var(--font-terminal);font-size:10px;color:var(--ink-4);
display:flex;align-items:center;gap:5px;user-select:none}
display:flex;align-items:center;justify-content:space-between;user-select:none}
.tile-foot-info{display:flex;align-items:center;gap:5px;min-width:0;overflow:hidden}
.btn-del-agent{border:none;background:transparent;cursor:pointer;flex-shrink:0;
color:var(--ink-5);font-size:11px;padding:2px 4px;border-radius:4px;
line-height:1;transition:color .15s,background .15s;user-select:none}
.btn-del-agent:hover{color:var(--err);background:color-mix(in srgb,var(--err) 12%,transparent)}
/* FOOTER */
.footer{background:var(--bg-0);border-top:1px solid var(--border-2);height:26px;
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Fond arrondi -->
<rect width="64" height="64" rx="14" fill="#282828"/>
<!-- Cercle accent -->
<circle cx="32" cy="32" r="22" fill="none" stroke="#fe8019" stroke-width="4"/>
<!-- Barre CPU style jauge -->
<rect x="18" y="28" width="28" height="4" rx="2" fill="#504945"/>
<rect x="18" y="28" width="18" height="4" rx="2" fill="#fe8019"/>
<!-- Point LED vert -->
<circle cx="43" cy="21" r="4" fill="#b8bb26"/>
</svg>

After

Width:  |  Height:  |  Size: 498 B

+13
View File
@@ -0,0 +1,13 @@
{
"name": "Nanometrics",
"short_name": "Nanometrics",
"description": "Tableau de bord de surveillance système",
"start_url": "/",
"display": "standalone",
"background_color": "#282828",
"theme_color": "#fe8019",
"icons": [
{ "src": "favicon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "favicon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
+30
View File
@@ -4,6 +4,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nanometrics</title>
<link rel="icon" type="image/x-icon" href="favicon/favicon.ico">
<link rel="icon" type="image/svg+xml" href="favicon/favicon.svg">
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16.png">
<link rel="apple-touch-icon" sizes="180x180" href="favicon/favicon-180.png">
<link rel="manifest" href="favicon/site.webmanifest">
<meta name="theme-color" content="#282828">
<link rel="stylesheet" href="vendor/fontawesome/css/all.min.css">
<link rel="stylesheet" href="css/app.css">
</head>
@@ -147,6 +154,29 @@
</div>
</div>
<!-- DIALOG SUPPRESSION AGENT -->
<div class="overlay" id="overlay-del" style="display:none;z-index:400" onclick="if(event.target===this)this.style.display='none'">
<div class="popup" style="width:360px;max-width:96vw" onclick="event.stopPropagation()">
<div style="padding:20px 20px 0">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">
<div style="width:36px;height:36px;border-radius:8px;background:color-mix(in srgb,var(--err) 15%,transparent);display:flex;align-items:center;justify-content:center;color:var(--err);font-size:16px;flex-shrink:0"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div>
<div style="font-weight:700;font-size:14px">Supprimer l'agent</div>
<div style="font-size:12px;color:var(--fg2);margin-top:2px">Cette action est irréversible</div>
</div>
</div>
<p style="font-size:13px;margin:0 0 8px">Supprimer <strong id="del-agent-name"></strong> ?<br>
<span style="font-size:11px;color:var(--fg2)">Toutes les métriques historiques seront effacées.</span></p>
</div>
<div style="padding:16px 20px;display:flex;justify-content:flex-end;gap:8px">
<button class="btn" onclick="document.getElementById('overlay-del').style.display='none'">Annuler</button>
<button class="btn" id="del-agent-confirm"
style="background:var(--err);color:#fff;border-color:var(--err)"
onclick="Popups.doDeleteAgent()"><i class="fa-solid fa-trash"></i> Supprimer</button>
</div>
</div>
</div>
<script src="js/api.js"></script>
<script src="js/charts.js"></script>
<script src="js/grid.js"></script>
+6
View File
@@ -27,6 +27,11 @@ const API = (() => {
if (!r.ok) throw new Error(`PUT ${path}: ${r.status}`);
}
async function del(path) {
const r = await fetch(BASE + path, { method: 'DELETE' });
if (!r.ok) throw new Error(`DELETE ${path}: ${r.status}`);
}
async function postForm(path, formData) {
const r = await fetch(BASE + path, { method: 'POST', body: formData });
if (!r.ok) throw new Error(`POST ${path}: ${r.status}`);
@@ -44,6 +49,7 @@ const API = (() => {
fd.append('icon', file);
return postForm(`/api/agents/${id}/icon`, fd);
},
deleteAgent: (id) => del(`/api/agents/${id}`),
iconUrl: (id) => `/api/agents/${id}/icon`,
};
})();
+23 -4
View File
@@ -81,7 +81,7 @@ const Grid = (() => {
<div class="g-ico" data-tip="RAM"><i class="fa-solid fa-memory"></i></div>
<div class="g-bar"><div class="g-fill ${offline ? '' : gFill(memPct ?? 0)}"
style="width:${offline ? 0 : (memPct ?? 0).toFixed(0)}%"></div></div>
<span class="g-val">${offline ? '—' : fmtPct(memPct)}</span>
<span class="g-val">${offline ? '—' : (metrics?.memory_used && metrics?.memory_total ? fmt(metrics.memory_used) + '/' + fmt(metrics.memory_total) : '—')}</span>
</div>
<div class="g-row">
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
@@ -91,9 +91,15 @@ const Grid = (() => {
</div>
</div>
<div class="tile-foot">
${offline
<span class="tile-foot-info">
${offline
? '<i class="fa-solid fa-circle-xmark" style="color:var(--err)"></i><span style="color:var(--err)">Hors ligne</span>'
: `<i class="fa-solid fa-clock"></i><span>${uptimeStr}</span>`}
: `<i class="fa-solid fa-clock"></i><span>${uptimeStr || '—'}</span>`}
</span>
<button class="btn-del-agent" title="Supprimer cet agent"
onclick="event.stopPropagation();Popups.confirmDeleteAgent('${esc(id)}','${esc(agent.hostname)}')">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>`;
}
@@ -101,6 +107,12 @@ const Grid = (() => {
function update(agentId, metrics) {
const entry = _agents.get(agentId);
if (!entry) return;
// Conserver les valeurs lentes (disque, smart) quand le paquet ne les contient pas
if (entry.metrics) {
for (const k of Object.keys(entry.metrics)) {
if (metrics[k] == null && entry.metrics[k] != null) metrics[k] = entry.metrics[k];
}
}
entry.metrics = metrics;
const el = document.getElementById('tile-' + agentId);
if (el) {
@@ -139,6 +151,13 @@ const Grid = (() => {
document.getElementById('stat-err').textContent = err;
}
function removeAgent(id) {
_agents.delete(id);
const el = document.getElementById('tile-' + id);
if (el) el.remove();
updateStats();
}
function getAgent(id) { return _agents.get(id); }
function updateStatus(agentId, status) {
@@ -150,5 +169,5 @@ const Grid = (() => {
updateStats();
}
return { refresh, update, updateStatus, getAgent, fmt, fmtPct };
return { refresh, update, updateStatus, removeAgent, getAgent, fmt, fmtPct };
})();
+38 -1
View File
@@ -305,6 +305,21 @@ const Popups = (() => {
${[[15,'15 min'],[30,'30 min'],[60,'1 heure'],[360,'6 heures']].map(([v,l]) =>
`<option value="${v}" ${(cfg.chart_duration_min??30)==v?'selected':''}>${l}</option>`).join('')}
</select></div>
</div>
<div style="display:flex;flex-direction:column;gap:8px">
<div class="scfg-sec-title">INSTALLATION AGENT</div>
<div class="scfg-row" style="flex-direction:column;align-items:stretch;gap:6px">
<label style="font-size:0.85em;color:var(--fg2)">Commande curl — copiez et lancez en root sur la machine cible</label>
<div style="display:flex;gap:6px;align-items:center">
<input type="text" id="s-install-cmd" readonly
style="flex:1;font-family:var(--font-mono);font-size:0.78em;padding:6px 8px;
background:var(--bg2);border:1px solid var(--border);border-radius:6px;
color:var(--fg);cursor:text;min-width:0"
value="SERVER_IP=${window.location.hostname} curl -fsSL https://git.maison43gil.com/gilles/nano_metrics/raw/branch/main/deploy/install.sh | sudo bash">
<button class="btn" style="padding:5px 10px;font-size:0.8em;white-space:nowrap"
onclick="navigator.clipboard.writeText(document.getElementById('s-install-cmd').value).then(()=>{this.textContent='✓ Copié';setTimeout(()=>this.textContent='Copier',1500)})">Copier</button>
</div>
</div>
</div>`;
document.getElementById('overlay-srvcfg').style.display = 'flex';
}
@@ -391,10 +406,32 @@ const Popups = (() => {
document.getElementById('overlay-smart').style.display = 'flex';
}
// ══ SUPPRESSION AGENT ══
let _delAgentId = null;
function confirmDeleteAgent(id, hostname) {
_delAgentId = id;
document.getElementById('del-agent-name').textContent = hostname || id;
document.getElementById('overlay-del').style.display = 'flex';
}
async function doDeleteAgent() {
if (!_delAgentId) return;
const id = _delAgentId;
document.getElementById('overlay-del').style.display = 'none';
_delAgentId = null;
try {
await API.deleteAgent(id);
Grid.removeAgent(id);
} catch (e) {
alert('Erreur lors de la suppression : ' + e.message);
}
}
return {
showDetail, hideDetail,
showAgentCfg, sendAgentConfig, toggleCbox,
showSrvCfg, hideSrvCfg, saveSrvCfg,
showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent,
showSmart,
};
})();
+5 -11
View File
@@ -82,18 +82,12 @@ ok "Binaire téléchargé ($(du -sh "$TMP_BIN" | cut -f1))"
echo ""
echo "--- Configuration du serveur ---"
if [ -z "${SERVER_IP:-}" ]; then
read -rp "Adresse IP du serveur Nanometrics : " SERVER_IP
fi
if [ -z "${SERVER_IP:-}" ]; then
err "SERVER_IP est requis."
exit 1
fi
SERVER_IP="${SERVER_IP:-10.0.0.50}"
SERVER_PORT="${SERVER_PORT:-9999}"
MQTT_HOST="${MQTT_HOST:-10.0.0.3}"
MQTT_ENABLED="${MQTT_ENABLED:-false}"
ok "Serveur : $SERVER_IP:$SERVER_PORT"
ok "Serveur : $SERVER_IP:$SERVER_PORT | MQTT broker : $MQTT_HOST"
# ── 5. Installer le binaire ────────────────────────────────────────────────────
echo ""
@@ -125,7 +119,7 @@ enabled = true
[protocols.mqtt]
enabled = $MQTT_ENABLED
host = "10.0.0.3"
host = "$MQTT_HOST"
port = 1883
topic_base = "nanometrics/agents"
auto_discovery = true
@@ -160,7 +154,7 @@ mqtt = false
udp = true
mqtt = false
TOML
chmod 640 "$CONFIG_FILE"
chmod 644 "$CONFIG_FILE"
ok "config.toml créé"
fi
+3 -1
View File
@@ -40,7 +40,9 @@ for i in "${!TARGETS[@]}"; do
# Installer la cible si absente
rustup target add "$TARGET" 2>/dev/null || true
# Compiler
# Compiler (CC requis par la crate ring pour les cibles musl cross)
CC_aarch64_unknown_linux_musl=aarch64-linux-gnu-gcc \
AR_aarch64_unknown_linux_musl=aarch64-linux-gnu-ar \
cargo build --release \
--manifest-path "$CARGO_TOML" \
--target "$TARGET" \
+14
View File
@@ -238,6 +238,20 @@ func (d *DB) MarkOffline(timeoutSec int64) error {
return err
}
func (d *DB) DeleteAgent(agentID string) error {
for _, q := range []string{
`DELETE FROM metrics WHERE agent_id = ?`,
`DELETE FROM agent_configs WHERE agent_id = ?`,
`DELETE FROM agent_icons WHERE agent_id = ?`,
`DELETE FROM agents WHERE id = ?`,
} {
if _, err := d.conn.Exec(q, agentID); err != nil {
return err
}
}
return nil
}
// MarkOfflineAndGetIDs marque les agents inactifs et retourne leurs IDs.
func (d *DB) MarkOfflineAndGetIDs(timeoutSec int64) ([]string, error) {
cutoff := time.Now().Unix() - timeoutSec
+17
View File
@@ -3,6 +3,7 @@ package handlers
import (
"encoding/json"
"net/http"
"strings"
"github.com/user/nanometrics/server/db"
)
@@ -18,3 +19,19 @@ func AgentsHandler(database *db.DB) http.HandlerFunc {
json.NewEncoder(w).Encode(agents)
}
}
func DeleteAgentHandler(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]
if err := database.DeleteAgent(agentID); err != nil {
http.Error(w, err.Error(), 500)
return
}
w.WriteHeader(http.StatusNoContent)
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ func AgentConfigHandler(database *db.DB, pushConfig func(agentID string, cfg *mo
return
}
if cfg == nil {
cfg = &models.AgentConfig{}
cfg = models.DefaultAgentConfig()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cfg)
+2
View File
@@ -110,6 +110,8 @@ func main() {
handlers.IconUploadHandler(database)(w, r)
case endsWith(r.URL.Path, "/icon") && r.Method == http.MethodGet:
handlers.IconGetHandler(database)(w, r)
case r.Method == http.MethodDelete:
handlers.DeleteAgentHandler(database)(w, r)
default:
http.NotFound(w, r)
}
+14
View File
@@ -88,6 +88,20 @@ type ServerConfig struct {
PopupDetailH int `json:"popup_detail_h"`
}
func DefaultAgentConfig() *AgentConfig {
on := MetricProto{UDP: true, MQTT: false}
return &AgentConfig{
Protocols: ProtocolsConfig{UDP: UDPConfig{Enabled: true}},
Metrics: MetricsConfig{
CPU: on,
Memory: on,
Disk: on,
Smart: on,
Uptime: on,
},
}
}
func DefaultServerConfig() ServerConfig {
return ServerConfig{
TileMinWidth: 220, FontSize: 13,