24 Commits
Author SHA1 Message Date
Gilles Soulier e65770407c chore(agent): bump version 0.1.2 → 0.1.3 2026-05-22 22:35:05 +02:00
Gilles SoulierandClaude Sonnet 4.6 9e77d961f5 feat(agent): déconnexion propre sur SIGTERM/SIGINT
- Capture SIGTERM et SIGINT via libc::signal → AtomicBool RUNNING
- La boucle principale s'arrête proprement à la prochaine itération
- Envoi d'un paquet status:offline via UDP avant de quitter
- MQTT : publish status offline + disconnect() pour déconnexion gracieuse
  (le last_will reste actif pour les déconnexions brutales)
- payload.rs: #[serde(default)] sur version pour compatibilité descendante

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:34:55 +02:00
Gilles SoulierandClaude Sonnet 4.6 3933301cff fix(db): GetLastMetrics retourne la dernière valeur non-nulle par colonne
La requête précédente prenait la dernière ligne (paquet rapide, 2s) qui
a hdd_*/smart_* à NULL. Chaque sous-requête cible maintenant la dernière
valeur non-nulle indépendamment, ce qui restitue les données disque/smart
au rechargement même si le dernier paquet ne les contenait pas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:29:06 +02:00
Gilles SoulierandClaude Sonnet 4.6 9f87c9294d revert(docker): retour au multi-stage, docker login requis pour le pull
Dockerfile multi-stage (golang:1.22-alpine → scratch) pour un build
autonome. docker-compose sans version obsolète, pull:false pour le
builder, pull_policy:if_not_present pour nginx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:12:56 +02:00
Gilles SoulierandClaude Sonnet 4.6 638d347bb0 fix(docker): évite les pulls Docker Hub inutiles (rate limit 429)
- Retire l'attribut version obsolète
- build.pull: false — BuildKit ne vérifie plus le manifest pour golang:1.22-alpine
- pull_policy: if_not_present — nginx:alpine n'est tiré que si absent du cache

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:06:41 +02:00
Gilles Soulier 8f3dbd0532 3 2026-05-22 22:06:12 +02:00
Gilles SoulierandClaude Sonnet 4.6 99bdf79a63 fix(docker): remplace alpine:3.19 par scratch pour éviter le rate limit
Le binaire est statique (CGO_ENABLED=0) — scratch suffit. Seuls les
certificats TLS sont copiés depuis le builder golang:1.22-alpine.
Élimine le pull de docker.io/library/alpine qui déclenche le 429.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:03:51 +02:00
Gilles SoulierandClaude Sonnet 4.6 a22d1f4cd2 fix(tile): icône personnalisée masque le fallback FA au chargement
Le span de fallback (fa-server) démarrait en display:flex — visible en
permanence derrière l'image. Il passe à display:none et n'est affiché
que si l'img déclenche onerror (pas d'icône).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:02:11 +02:00
Gilles SoulierandClaude Sonnet 4.6 d8f395cb53 feat(dashboard): métriques chargées immédiatement au rechargement de page
GET /api/agents inclut désormais last_metrics (dernière ligne de la table
metrics) pour chaque agent. grid.js l'utilise lors du refresh initial, ce
qui peuple les tuiles sans attendre le prochain message WebSocket.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 22:00:33 +02:00
Gilles SoulierandClaude Sonnet 4.6 f69c22039b fix(icon): upload d'icône — retour d'erreur, WEBP, limite Nginx
- nginx: client_max_body_size 10m (limite par défaut 1 Mo bloquait les images)
- icons.go: import _ golang.org/x/image/webp et image/gif pour décoder WEBP/GIF
- index.html: retire SVG de l'accept (serveur le rejette) et corrige le hint
- popups.js: try/catch autour de uploadIcon → message d'erreur visible dans le hint
  pendant 4s si l'upload échoue ; reset du file input pour re-sélectionner le même
  fichier ; rafraîchit l'img de la tuile avec cache-busting après succès

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:58:46 +02:00
Gilles SoulierandClaude Sonnet 4.6 2bda420728 feat(dashboard): affichage disque en Go utilisé/total comme la RAM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:51:17 +02:00
Gilles SoulierandClaude Sonnet 4.6 f604e22f6e fix(deploy): permissions config et prompt d'écrasement au réinstall
- ConfigurationDirectoryMode 0750→0755 : le DynamicUser (sans groupe root)
  peut maintenant traverser /etc/nanometrics et lire config.toml
- chmod 644 systématique sur config.toml même si conservé (corrige les
  anciennes installs en 640 qui causent un PermissionDenied au démarrage)
- Prompt interactif si config existe : o=écraser, N=conserver ; variable
  OVERWRITE_CONFIG=true pour forcer sans interaction (curl|bash)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 21:49:13 +02:00
Gilles SoulierandClaude Sonnet 4.6 8d4dc0e853 fix(deploy): arrêt du service avant remplacement du binaire
Évite l'erreur "Fichier texte occupé" lors d'une mise à jour à chaud.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:32:17 +02:00
Gilles Soulier 311bdbc66d chore: version agent 0.1.2 2026-05-22 20:28:30 +02:00
Gilles SoulierandClaude Sonnet 4.6 0df716b8b0 feat: version agent remontée au serveur et affichée dans la popup
- payload.rs : champ version (env!("CARGO_PKG_VERSION"))
- models.go  : Version dans AgentMetrics et Agent
- db.go      : colonne version dans agents + migration ALTER TABLE
- popups.js  : badge version dans la section INFORMATIONS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:27:26 +02:00
Gilles SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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 SoulierandClaude Sonnet 4.6 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
42 changed files with 576 additions and 176 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 # OS
.DS_Store .DS_Store
Thumbs.db 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"
+130 -99
View File
@@ -38,9 +38,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.10.1" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [ dependencies = [
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -97,21 +97,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 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]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.32" version = "0.3.32"
@@ -142,6 +127,17 @@ dependencies = [
"slab", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.4.2" version = "0.4.2"
@@ -247,13 +243,14 @@ checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
name = "nanometrics-agent" name = "nanometrics-agent"
version = "0.1.0" version = "0.1.2"
dependencies = [ dependencies = [
"libc",
"rumqttc", "rumqttc",
"serde", "serde",
"serde_json", "serde_json",
@@ -262,23 +259,6 @@ dependencies = [
"toml", "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]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.4.3" version = "0.4.3"
@@ -294,48 +274,11 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" 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]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.2.1" version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[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",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
@@ -343,12 +286,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@@ -383,6 +320,20 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" 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]] [[package]]
name = "rumqttc" name = "rumqttc"
version = "0.24.0" version = "0.24.0"
@@ -393,10 +344,12 @@ dependencies = [
"flume", "flume",
"futures-util", "futures-util",
"log", "log",
"native-tls", "rustls-native-certs",
"rustls-pemfile",
"rustls-webpki",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-native-tls", "tokio-rustls",
] ]
[[package]] [[package]]
@@ -409,7 +362,63 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "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]] [[package]]
@@ -418,7 +427,7 @@ version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -429,9 +438,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.7.0" version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"core-foundation", "core-foundation",
@@ -527,7 +536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -539,6 +548,12 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@@ -571,10 +586,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom", "getrandom 0.4.2",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -609,7 +624,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -624,12 +639,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "tokio-native-tls" name = "tokio-rustls"
version = "0.3.1" version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [ dependencies = [
"native-tls", "rustls",
"rustls-pki-types",
"tokio", "tokio",
] ]
@@ -687,10 +703,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "vcpkg" name = "untrusted"
version = "0.2.15" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "wasi" name = "wasi"
@@ -797,6 +813,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -973,6 +998,12 @@ dependencies = [
"wasmparser", "wasmparser",
] ]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"
+3 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nanometrics-agent" name = "nanometrics-agent"
version = "0.1.0" version = "0.1.3"
edition = "2021" edition = "2021"
[lib] [lib]
@@ -19,10 +19,11 @@ codegen-units = 1
[dependencies] [dependencies]
sysinfo = { version = "0.30", default-features = false } sysinfo = { version = "0.30", default-features = false }
libc = "0.2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
toml = "0.8" 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] [dev-dependencies]
tempfile = "3" tempfile = "3"
+36 -5
View File
@@ -1,7 +1,14 @@
use nanometrics_agent::{config, metrics, payload, transport}; 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::time::{Duration, Instant};
use std::sync::mpsc; use std::sync::mpsc;
use std::sync::atomic::{AtomicBool, Ordering};
static RUNNING: AtomicBool = AtomicBool::new(true);
extern "C" fn handle_signal(_: libc::c_int) {
RUNNING.store(false, Ordering::Relaxed);
}
fn get_local_ip() -> String { fn get_local_ip() -> String {
use std::net::UdpSocket; use std::net::UdpSocket;
@@ -34,7 +41,6 @@ fn main() {
let mut sys = System::new(); let mut sys = System::new();
let mut networks = Networks::new_with_refreshed_list(); 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 mut components = Components::new_with_refreshed_list();
let udp_sender = if cfg.protocols.udp.enabled { let udp_sender = if cfg.protocols.udp.enabled {
@@ -50,12 +56,17 @@ fn main() {
None None
}; };
unsafe {
libc::signal(libc::SIGTERM, handle_signal as libc::sighandler_t);
libc::signal(libc::SIGINT, handle_signal as libc::sighandler_t);
}
let mut last_slow = Instant::now(); let mut last_slow = Instant::now();
let mut last_medium = Instant::now(); let mut last_medium = Instant::now();
let mut first_medium = true; let mut first_medium = true;
let mut first_slow = true; let mut first_slow = true;
loop { while RUNNING.load(Ordering::Relaxed) {
let now = Instant::now(); let now = Instant::now();
while let Ok(transport::mqtt::MqttIncoming::ConfigUpdate(data)) = incoming_rx.try_recv() { while let Ok(transport::mqtt::MqttIncoming::ConfigUpdate(data)) = incoming_rx.try_recv() {
@@ -69,6 +80,7 @@ fn main() {
hostname: hostname.clone(), hostname: hostname.clone(),
ip: ip.clone(), ip: ip.clone(),
status: "online".to_string(), status: "online".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
..Default::default() ..Default::default()
}; };
@@ -102,9 +114,8 @@ fn main() {
} }
if first_slow || now.duration_since(last_slow).as_secs() >= 60 { if first_slow || now.duration_since(last_slow).as_secs() >= 60 {
disks.refresh();
if cfg.metrics.disk.udp || cfg.metrics.disk.mqtt { 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_used = Some(used);
m.hdd_free = Some(free); m.hdd_free = Some(free);
m.hdd_total = Some(total); m.hdd_total = Some(total);
@@ -135,4 +146,24 @@ fn main() {
std::thread::sleep(Duration::from_secs(2)); std::thread::sleep(Duration::from_secs(2));
} }
// Déconnexion propre : notifier le serveur avant de quitter
let offline = serde_json::to_string(&payload::AgentMetrics {
hostname: hostname.clone(),
ip: ip.clone(),
status: "offline".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
..Default::default()
}).unwrap_or_default();
if let Some(ref udp) = udp_sender {
udp.send(&offline);
}
if let Some(ref client) = mqtt_client {
transport::mqtt::publish_status(
client, &cfg.protocols.mqtt.topic_base, &hostname, "offline",
);
std::thread::sleep(Duration::from_millis(200)); // laisser le temps au broker de recevoir
let _ = client.disconnect();
}
} }
+22 -20
View File
@@ -1,34 +1,36 @@
use sysinfo::Disks; use std::mem::MaybeUninit;
pub fn get(disks: &Disks) -> (u64, u64, u64) { /// Retourne (used, free, total) en octets pour le système de fichiers racine "/".
for disk in disks.list() { /// Utilise statvfs() directement pour correspondre exactement aux chiffres de `df` :
let mount = disk.mount_point().to_string_lossy(); /// - total = f_blocks × f_frsize
if mount == "/" { /// - used = (f_blocks f_bfree) × f_frsize (blocs effectivement écrits)
let total = disk.total_space(); /// - free = f_bavail × f_frsize (disponible pour utilisateurs non-root)
let free = disk.available_space(); pub fn get() -> (u64, u64, u64) {
let used = total.saturating_sub(free); let path = b"/\0";
return (used, free, total); 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 stat = unsafe { stat.assume_init() };
let total = disk.total_space(); let bsize = stat.f_frsize as u64;
let free = disk.available_space(); let total = stat.f_blocks.saturating_mul(bsize);
return (total.saturating_sub(free), free, total); let used = stat.f_blocks.saturating_sub(stat.f_bfree).saturating_mul(bsize);
} let free = stat.f_bavail.saturating_mul(bsize);
(0, 0, 0) (used, free, total)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use sysinfo::Disks;
#[test] #[test]
fn test_disk_coherent() { fn test_disk_coherent() {
let disks = Disks::new_with_refreshed_list(); let (used, free, total) = get();
let (used, free, total) = get(&disks); eprintln!("résultat statvfs : used={used} free={free} total={total}");
if total > 0 { 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");
} }
} }
} }
+2
View File
@@ -5,6 +5,8 @@ pub struct AgentMetrics {
pub hostname: String, pub hostname: String,
pub ip: String, pub ip: String,
pub status: String, pub status: String,
#[serde(default)]
pub version: String,
pub cpu_percent: Option<f32>, pub cpu_percent: Option<f32>,
pub memory_used: Option<u64>, pub memory_used: Option<u64>,
pub memory_free: Option<u64>, pub memory_free: Option<u64>,
+5
View File
@@ -78,3 +78,8 @@ pub fn publish_metrics(client: &Client, topic_base: &str, hostname: &str, json:
let topic = format!("{}/{}/metrics", topic_base, hostname); let topic = format!("{}/{}/metrics", topic_base, hostname);
let _ = client.publish(topic, QoS::AtMostOnce, false, json); let _ = client.publish(topic, QoS::AtMostOnce, false, json);
} }
pub fn publish_status(client: &Client, topic_base: &str, hostname: &str, status: &str) {
let topic = format!("{}/{}/status", topic_base, hostname);
let _ = client.publish(topic, QoS::AtLeastOnce, true, status);
}
+1
View File
@@ -18,6 +18,7 @@ fn test_serialize_json_complet() {
temperature: None, temperature: None,
smart: None, smart: None,
status: "online".to_string(), status: "online".to_string(),
version: "0.0.0".to_string(),
}; };
let json = serde_json::to_string(&m).unwrap(); let json = serde_json::to_string(&m).unwrap();
assert!(json.contains("\"hostname\":\"srv-01\"")); assert!(json.contains("\"hostname\":\"srv-01\""));
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+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-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{height:100%;border-radius:3px;background:var(--ok);transition:width .3s}
.g-fill.w{background:var(--warn)}.g-fill.e{background:var(--err)} .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); .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 */
.footer{background:var(--bg-0);border-top:1px solid var(--border-2);height:26px; .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" }
]
}
+33 -3
View File
@@ -4,6 +4,13 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nanometrics</title> <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="vendor/fontawesome/css/all.min.css">
<link rel="stylesheet" href="css/app.css"> <link rel="stylesheet" href="css/app.css">
</head> </head>
@@ -66,12 +73,12 @@
<img id="pop-icon-img" src="" alt="" style="display:none"> <img id="pop-icon-img" src="" alt="" style="display:none">
<div class="agent-icon-overlay"><i class="fa-solid fa-camera"></i><span>Changer</span></div> <div class="agent-icon-overlay"><i class="fa-solid fa-camera"></i><span>Changer</span></div>
</div> </div>
<input type="file" id="icon-upload" accept=".svg,.jpg,.jpeg,.png,.webp" style="display:none"> <input type="file" id="icon-upload" accept=".jpg,.jpeg,.png,.webp" style="display:none">
<div style="flex:1"> <div style="flex:1">
<div class="pop-host" id="pop-host"></div> <div class="pop-host" id="pop-host"></div>
<div class="pop-ip" id="pop-ip"></div> <div class="pop-ip" id="pop-ip"></div>
<div style="font-size:10px;color:var(--ink-4);font-family:var(--font-terminal);margin-top:2px"> <div id="icon-hint" style="font-size:10px;color:var(--ink-4);font-family:var(--font-terminal);margin-top:2px">
Cliquer sur l'icône pour personnaliser · SVG JPG PNG WEBP · max 128×128 px Cliquer sur l'icône pour personnaliser · JPG PNG WEBP · max 128×128 px
</div> </div>
</div> </div>
<div class="pop-led" id="pop-led"></div> <div class="pop-led" id="pop-led"></div>
@@ -147,6 +154,29 @@
</div> </div>
</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/api.js"></script>
<script src="js/charts.js"></script> <script src="js/charts.js"></script>
<script src="js/grid.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}`); 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) { async function postForm(path, formData) {
const r = await fetch(BASE + path, { method: 'POST', body: formData }); const r = await fetch(BASE + path, { method: 'POST', body: formData });
if (!r.ok) throw new Error(`POST ${path}: ${r.status}`); if (!r.ok) throw new Error(`POST ${path}: ${r.status}`);
@@ -44,6 +49,7 @@ const API = (() => {
fd.append('icon', file); fd.append('icon', file);
return postForm(`/api/agents/${id}/icon`, fd); return postForm(`/api/agents/${id}/icon`, fd);
}, },
deleteAgent: (id) => del(`/api/agents/${id}`),
iconUrl: (id) => `/api/agents/${id}/icon`, iconUrl: (id) => `/api/agents/${id}/icon`,
}; };
})(); })();
+26 -7
View File
@@ -58,7 +58,7 @@ const Grid = (() => {
const iconContent = `<img src="${API.iconUrl(id)}" alt="" const iconContent = `<img src="${API.iconUrl(id)}" alt=""
style="width:100%;height:100%;object-fit:cover;border-radius:7px" style="width:100%;height:100%;object-fit:cover;border-radius:7px"
onerror="this.style.display='none';this.nextSibling.style.display='flex'"> onerror="this.style.display='none';this.nextSibling.style.display='flex'">
<span style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)"> <span style="display:none;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)">
<i class="fa-solid fa-server"></i></span>`; <i class="fa-solid fa-server"></i></span>`;
return `<div class="tile ${sc}" id="tile-${id}" onclick="Popups.showDetail('${esc(id)}')"> return `<div class="tile ${sc}" id="tile-${id}" onclick="Popups.showDetail('${esc(id)}')">
@@ -81,19 +81,25 @@ const Grid = (() => {
<div class="g-ico" data-tip="RAM"><i class="fa-solid fa-memory"></i></div> <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)}" <div class="g-bar"><div class="g-fill ${offline ? '' : gFill(memPct ?? 0)}"
style="width:${offline ? 0 : (memPct ?? 0).toFixed(0)}%"></div></div> 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>
<div class="g-row"> <div class="g-row">
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div> <div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
<div class="g-bar"><div class="g-fill ${offline ? '' : (diskPct >= (App.serverConfig?.warn_disk ?? 75) ? 'w' : '')}" <div class="g-bar"><div class="g-fill ${offline ? '' : (diskPct >= (App.serverConfig?.warn_disk ?? 75) ? 'w' : '')}"
style="width:${offline ? 0 : (diskPct ?? 0).toFixed(0)}%"></div></div> style="width:${offline ? 0 : (diskPct ?? 0).toFixed(0)}%"></div></div>
<span class="g-val">${offline ? '—' : fmtPct(diskPct)}</span> <span class="g-val">${offline ? '—' : (metrics?.hdd_used && metrics?.hdd_total ? fmt(metrics.hdd_used) + '/' + fmt(metrics.hdd_total) : '—')}</span>
</div> </div>
</div> </div>
<div class="tile-foot"> <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-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>
</div>`; </div>`;
} }
@@ -101,6 +107,12 @@ const Grid = (() => {
function update(agentId, metrics) { function update(agentId, metrics) {
const entry = _agents.get(agentId); const entry = _agents.get(agentId);
if (!entry) return; 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; entry.metrics = metrics;
const el = document.getElementById('tile-' + agentId); const el = document.getElementById('tile-' + agentId);
if (el) { if (el) {
@@ -112,7 +124,7 @@ const Grid = (() => {
function refresh(agents) { function refresh(agents) {
agents.forEach(a => { agents.forEach(a => {
if (!_agents.has(a.id)) { if (!_agents.has(a.id)) {
_agents.set(a.id, { agent: a, metrics: null }); _agents.set(a.id, { agent: a, metrics: a.last_metrics || null });
} else { } else {
_agents.get(a.id).agent = a; _agents.get(a.id).agent = a;
} }
@@ -139,6 +151,13 @@ const Grid = (() => {
document.getElementById('stat-err').textContent = err; 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 getAgent(id) { return _agents.get(id); }
function updateStatus(agentId, status) { function updateStatus(agentId, status) {
@@ -150,5 +169,5 @@ const Grid = (() => {
updateStats(); updateStats();
} }
return { refresh, update, updateStatus, getAgent, fmt, fmtPct }; return { refresh, update, updateStatus, removeAgent, getAgent, fmt, fmtPct };
})(); })();
+62 -3
View File
@@ -30,8 +30,27 @@ const Popups = (() => {
document.getElementById('icon-upload').onchange = async (e) => { document.getElementById('icon-upload').onchange = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
await API.uploadIcon(agentId, file); const hint = document.getElementById('icon-hint');
img.src = API.iconUrl(agentId) + '?t=' + Date.now(); try {
await API.uploadIcon(agentId, file);
const ts = '?t=' + Date.now();
img.src = API.iconUrl(agentId) + ts;
img.style.display = 'block';
document.getElementById('pop-icon-fa').style.display = 'none';
const tileImg = document.querySelector(`#tile-${CSS.escape(agentId)} .t-icon img`);
if (tileImg) tileImg.src = API.iconUrl(agentId) + ts;
} catch (err) {
if (hint) {
hint.style.color = 'var(--err)';
hint.textContent = 'Erreur : ' + (err.message || 'téléversement échoué');
setTimeout(() => {
hint.style.color = '';
hint.textContent = 'Cliquer sur l\'icône pour personnaliser · JPG PNG WEBP · max 128×128 px';
}, 4000);
}
} finally {
e.target.value = '';
}
}; };
// Uptime // Uptime
@@ -119,6 +138,9 @@ const Popups = (() => {
<div class="meta-grid"> <div class="meta-grid">
<div class="meta"><div class="meta-lbl">HOSTNAME</div><div class="meta-val">${esc(agent.hostname)}</div></div> <div class="meta"><div class="meta-lbl">HOSTNAME</div><div class="meta-val">${esc(agent.hostname)}</div></div>
<div class="meta"><div class="meta-lbl">ADRESSE IP</div><div class="meta-val">${esc(agent.ip) || '—'}</div></div> <div class="meta"><div class="meta-lbl">ADRESSE IP</div><div class="meta-val">${esc(agent.ip) || '—'}</div></div>
<div class="meta"><div class="meta-lbl">VERSION AGENT</div><div class="meta-val" style="display:flex;align-items:center;gap:6px">
${agent.version ? `<span style="font-family:var(--font-mono);background:var(--bg-1);border:1px solid var(--border-2);border-radius:5px;padding:1px 7px;font-size:11px;color:var(--accent)">v${esc(agent.version)}</span>` : '<span style="color:var(--ink-4)">—</span>'}
</div></div>
<div class="meta"><div class="meta-lbl">PROTOCOLES ACTIFS</div><div style="display:flex;gap:5px;margin-top:4px">${protos || '—'}</div></div> <div class="meta"><div class="meta-lbl">PROTOCOLES ACTIFS</div><div style="display:flex;gap:5px;margin-top:4px">${protos || '—'}</div></div>
<div class="meta"><div class="meta-lbl">DERNIER CONTACT</div><div class="meta-val">${new Date(agent.last_seen * 1000).toLocaleTimeString('fr-FR')}</div></div> <div class="meta"><div class="meta-lbl">DERNIER CONTACT</div><div class="meta-val">${new Date(agent.last_seen * 1000).toLocaleTimeString('fr-FR')}</div></div>
</div> </div>
@@ -305,6 +327,21 @@ const Popups = (() => {
${[[15,'15 min'],[30,'30 min'],[60,'1 heure'],[360,'6 heures']].map(([v,l]) => ${[[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('')} `<option value="${v}" ${(cfg.chart_duration_min??30)==v?'selected':''}>${l}</option>`).join('')}
</select></div> </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>`; </div>`;
document.getElementById('overlay-srvcfg').style.display = 'flex'; document.getElementById('overlay-srvcfg').style.display = 'flex';
} }
@@ -391,10 +428,32 @@ const Popups = (() => {
document.getElementById('overlay-smart').style.display = 'flex'; 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 { return {
showDetail, hideDetail, showDetail, hideDetail,
showAgentCfg, sendAgentConfig, toggleCbox, showAgentCfg, sendAgentConfig, toggleCbox,
showSrvCfg, hideSrvCfg, saveSrvCfg, showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent,
showSmart, showSmart,
}; };
})(); })();
+42 -14
View File
@@ -82,22 +82,23 @@ ok "Binaire téléchargé ($(du -sh "$TMP_BIN" | cut -f1))"
echo "" echo ""
echo "--- Configuration du serveur ---" echo "--- Configuration du serveur ---"
if [ -z "${SERVER_IP:-}" ]; then SERVER_IP="${SERVER_IP:-10.0.0.50}"
read -rp "Adresse IP du serveur Nanometrics : " SERVER_IP
fi
if [ -z "${SERVER_IP:-}" ]; then
err "SERVER_IP est requis."
exit 1
fi
SERVER_PORT="${SERVER_PORT:-9999}" SERVER_PORT="${SERVER_PORT:-9999}"
MQTT_HOST="${MQTT_HOST:-10.0.0.3}"
MQTT_ENABLED="${MQTT_ENABLED:-false}" 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 ──────────────────────────────────────────────────── # ── 5. Installer le binaire ────────────────────────────────────────────────────
echo "" echo ""
echo "[1/5] Installation du binaire dans /usr/local/bin/" echo "[1/5] Installation du binaire dans /usr/local/bin/"
# Arrêter le service si en cours (le binaire ne peut pas être écrasé à chaud)
if systemctl is-active --quiet nanometrics-agent 2>/dev/null; then
warn "Service en cours — arrêt temporaire..."
systemctl stop nanometrics-agent
fi
cp "$TMP_BIN" "$INSTALL_BIN" cp "$TMP_BIN" "$INSTALL_BIN"
chmod 755 "$INSTALL_BIN" chmod 755 "$INSTALL_BIN"
ok "Binaire installé" ok "Binaire installé"
@@ -111,10 +112,33 @@ ok "Répertoire créé"
# ── 7. Écrire config.toml ───────────────────────────────────────────────────── # ── 7. Écrire config.toml ─────────────────────────────────────────────────────
echo "[3/5] Écriture de $CONFIG_FILE" echo "[3/5] Écriture de $CONFIG_FILE"
# Ne pas écraser une config existante (upgrade) OVERWRITE_CONFIG="${OVERWRITE_CONFIG:-}"
WRITE_CONFIG=true
if [ -f "$CONFIG_FILE" ]; then if [ -f "$CONFIG_FILE" ]; then
warn "config.toml déjà présent — conservé tel quel" if [ "${OVERWRITE_CONFIG}" = "true" ]; then
else warn "OVERWRITE_CONFIG=true — config.toml sera écrasé"
WRITE_CONFIG=true
elif [ -t 0 ]; then
# Mode interactif (bash local, pas curl | bash)
echo ""
warn "Un config.toml existe déjà :"
echo " $CONFIG_FILE"
printf " Écraser la configuration existante ? [o/N] : "
read -r _ANS
if [[ "$_ANS" =~ ^[Oo]$ ]]; then
WRITE_CONFIG=true
else
ok "config.toml conservé"
WRITE_CONFIG=false
fi
else
warn "config.toml déjà présent — conservé (relancez avec OVERWRITE_CONFIG=true pour écraser)"
WRITE_CONFIG=false
fi
fi
if [ "$WRITE_CONFIG" = "true" ]; then
cat > "$CONFIG_FILE" << TOML cat > "$CONFIG_FILE" << TOML
[server] [server]
ip = "$SERVER_IP" ip = "$SERVER_IP"
@@ -125,7 +149,7 @@ enabled = true
[protocols.mqtt] [protocols.mqtt]
enabled = $MQTT_ENABLED enabled = $MQTT_ENABLED
host = "10.0.0.3" host = "$MQTT_HOST"
port = 1883 port = 1883
topic_base = "nanometrics/agents" topic_base = "nanometrics/agents"
auto_discovery = true auto_discovery = true
@@ -160,10 +184,14 @@ mqtt = false
udp = true udp = true
mqtt = false mqtt = false
TOML TOML
chmod 640 "$CONFIG_FILE" chmod 644 "$CONFIG_FILE"
ok "config.toml créé" ok "config.toml créé"
fi fi
# S'assurer que le fichier est toujours lisible (cas d'un config existant en 640)
chmod 644 "$CONFIG_FILE" 2>/dev/null || true
chmod 755 "$CONFIG_DIR"
# ── 8. Installer le fichier service ────────────────────────────────────────── # ── 8. Installer le fichier service ──────────────────────────────────────────
echo "[4/5] Installation du service systemd" echo "[4/5] Installation du service systemd"
curl -fsSL -o "$SERVICE_FILE" "$SERVICE_URL" curl -fsSL -o "$SERVICE_FILE" "$SERVICE_URL"
+1 -1
View File
@@ -11,7 +11,7 @@ RestartSec=5
DynamicUser=yes DynamicUser=yes
ConfigurationDirectory=nanometrics ConfigurationDirectory=nanometrics
ConfigurationDirectoryMode=0750 ConfigurationDirectoryMode=0755
ProtectSystem=strict ProtectSystem=strict
ProtectHome=read-only ProtectHome=read-only
+6 -2
View File
@@ -25,7 +25,9 @@ DESCRIPTION="${2:-Release $TAG}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CARGO_TOML="$ROOT/agent/Cargo.toml" CARGO_TOML="$ROOT/agent/Cargo.toml"
# ── 1. Compiler pour toutes les cibles supportées ────────────────────────── mkdir -p "$ROOT/dist"
# ── 1. Compiler l'agent pour toutes les cibles supportées ────────────────
echo "=== Compilation de l'agent ===" echo "=== Compilation de l'agent ==="
TARGETS=("x86_64-unknown-linux-musl" "aarch64-unknown-linux-musl") TARGETS=("x86_64-unknown-linux-musl" "aarch64-unknown-linux-musl")
LABELS=("linux-amd64" "linux-arm64") LABELS=("linux-amd64" "linux-arm64")
@@ -40,7 +42,9 @@ for i in "${!TARGETS[@]}"; do
# Installer la cible si absente # Installer la cible si absente
rustup target add "$TARGET" 2>/dev/null || true 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 \ cargo build --release \
--manifest-path "$CARGO_TOML" \ --manifest-path "$CARGO_TOML" \
--target "$TARGET" \ --target "$TARGET" \
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+4 -5
View File
@@ -5,10 +5,9 @@ RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o nanometrics-server . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o nanometrics-server .
FROM alpine:3.19 FROM scratch
RUN apk add --no-cache ca-certificates COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
WORKDIR /app COPY --from=builder /app/nanometrics-server /nanometrics-server
COPY --from=builder /app/nanometrics-server .
VOLUME /data VOLUME /data
EXPOSE 8080 9999/udp EXPOSE 8080 9999/udp
CMD ["./nanometrics-server"] CMD ["/nanometrics-server"]
+94 -9
View File
@@ -20,7 +20,7 @@ const schema = `
CREATE TABLE IF NOT EXISTS agents ( CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY, hostname TEXT NOT NULL, id TEXT PRIMARY KEY, hostname TEXT NOT NULL,
ip TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'offline', ip TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'offline',
last_seen INTEGER NOT NULL DEFAULT 0 last_seen INTEGER NOT NULL DEFAULT 0, version TEXT NOT NULL DEFAULT ''
); );
CREATE TABLE IF NOT EXISTS metrics ( CREATE TABLE IF NOT EXISTS metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, ts INTEGER NOT NULL, id INTEGER PRIMARY KEY AUTOINCREMENT, agent_id TEXT NOT NULL, ts INTEGER NOT NULL,
@@ -57,8 +57,12 @@ func Open(path string) (*DB, error) {
} }
func (d *DB) migrate() error { func (d *DB) migrate() error {
_, err := d.conn.Exec(schema) if _, err := d.conn.Exec(schema); err != nil {
return err return err
}
// Migrations additives pour les colonnes ajoutées après la création initiale
_, _ = d.conn.Exec(`ALTER TABLE agents ADD COLUMN version TEXT NOT NULL DEFAULT ''`)
return nil
} }
func (d *DB) Close() { _ = d.conn.Close() } func (d *DB) Close() { _ = d.conn.Close() }
@@ -66,11 +70,12 @@ func (d *DB) Close() { _ = d.conn.Close() }
func (d *DB) UpsertAgent(m *models.AgentMetrics) error { func (d *DB) UpsertAgent(m *models.AgentMetrics) error {
ts := time.Now().Unix() ts := time.Now().Unix()
_, err := d.conn.Exec(` _, err := d.conn.Exec(`
INSERT INTO agents (id, hostname, ip, status, last_seen) INSERT INTO agents (id, hostname, ip, status, last_seen, version)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
ip=excluded.ip, status=excluded.status, last_seen=excluded.last_seen`, ip=excluded.ip, status=excluded.status, last_seen=excluded.last_seen,
m.Hostname, m.Hostname, m.IP, m.Status, ts) version=CASE WHEN excluded.version != '' THEN excluded.version ELSE version END`,
m.Hostname, m.Hostname, m.IP, m.Status, ts, m.Version)
return err return err
} }
@@ -104,7 +109,7 @@ func (d *DB) InsertMetrics(m *models.AgentMetrics) error {
} }
func (d *DB) GetAgents() ([]models.Agent, error) { func (d *DB) GetAgents() ([]models.Agent, error) {
rows, err := d.conn.Query(`SELECT id, hostname, ip, status, last_seen FROM agents`) rows, err := d.conn.Query(`SELECT id, hostname, ip, status, last_seen, version FROM agents`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -112,7 +117,7 @@ func (d *DB) GetAgents() ([]models.Agent, error) {
var agents []models.Agent var agents []models.Agent
for rows.Next() { for rows.Next() {
var a models.Agent var a models.Agent
if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Status, &a.LastSeen); err != nil { if err := rows.Scan(&a.ID, &a.Hostname, &a.IP, &a.Status, &a.LastSeen, &a.Version); err != nil {
return nil, err return nil, err
} }
agents = append(agents, a) agents = append(agents, a)
@@ -123,6 +128,72 @@ func (d *DB) GetAgents() ([]models.Agent, error) {
return agents, nil return agents, nil
} }
func (d *DB) GetLastMetrics(agentID string) (*models.AgentMetrics, error) {
var cpu, temperature *float64
var memUsed, memFree, memTotal, hddUsed, hddFree, hddTotal *int64
var uptime, netRX, netTX *int64
var smartPassed, smartTemp, smartRealloc, smartHours, smartWear *int64
// Chaque sous-requête prend la dernière valeur NON NULL de sa colonne.
// Nécessaire car les paquets rapides (2s) ne contiennent pas les métriques
// lentes (disque, smart) qui sont envoyées toutes les 60s.
err := d.conn.QueryRow(`
SELECT
(SELECT cpu_percent FROM metrics WHERE agent_id=? AND cpu_percent IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT memory_used FROM metrics WHERE agent_id=? AND memory_used IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT memory_free FROM metrics WHERE agent_id=? AND memory_free IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT memory_total FROM metrics WHERE agent_id=? AND memory_total IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT hdd_used FROM metrics WHERE agent_id=? AND hdd_used IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT hdd_free FROM metrics WHERE agent_id=? AND hdd_free IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT hdd_total FROM metrics WHERE agent_id=? AND hdd_total IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT uptime FROM metrics WHERE agent_id=? AND uptime IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT network_rx FROM metrics WHERE agent_id=? AND network_rx IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT network_tx FROM metrics WHERE agent_id=? AND network_tx IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT temperature FROM metrics WHERE agent_id=? AND temperature IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT smart_passed FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT smart_temp FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT smart_realloc FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT smart_hours FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1),
(SELECT smart_wear FROM metrics WHERE agent_id=? AND smart_passed IS NOT NULL ORDER BY ts DESC LIMIT 1)`,
agentID, agentID, agentID, agentID,
agentID, agentID, agentID,
agentID, agentID, agentID, agentID,
agentID, agentID, agentID, agentID, agentID).
Scan(&cpu, &memUsed, &memFree, &memTotal,
&hddUsed, &hddFree, &hddTotal,
&uptime, &netRX, &netTX, &temperature,
&smartPassed, &smartTemp, &smartRealloc, &smartHours, &smartWear)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
m := &models.AgentMetrics{
CPUPercent: cpu,
MemoryUsed: memUsed,
MemoryFree: memFree,
MemoryTotal: memTotal,
HDDUsed: hddUsed,
HDDFree: hddFree,
HDDTotal: hddTotal,
Uptime: uptime,
NetworkRX: netRX,
NetworkTX: netTX,
Temperature: temperature,
}
if smartPassed != nil {
m.Smart = &models.SmartMetrics{
Passed: *smartPassed == 1,
Temperature: smartTemp,
ReallocatedSectors: smartRealloc,
PowerOnHours: smartHours,
WearLevel: smartWear,
}
}
return m, nil
}
func (d *DB) GetMetricsHistory(agentID string, from, to int64) ([]map[string]interface{}, error) { func (d *DB) GetMetricsHistory(agentID string, from, to int64) ([]map[string]interface{}, error) {
rows, err := d.conn.Query(` rows, err := d.conn.Query(`
SELECT ts, cpu_percent, memory_used, memory_total, hdd_used, hdd_total SELECT ts, cpu_percent, memory_used, memory_total, hdd_used, hdd_total
@@ -238,6 +309,20 @@ func (d *DB) MarkOffline(timeoutSec int64) error {
return err 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. // MarkOfflineAndGetIDs marque les agents inactifs et retourne leurs IDs.
func (d *DB) MarkOfflineAndGetIDs(timeoutSec int64) ([]string, error) { func (d *DB) MarkOfflineAndGetIDs(timeoutSec int64) ([]string, error) {
cutoff := time.Now().Unix() - timeoutSec cutoff := time.Now().Unix() - timeoutSec
+4 -2
View File
@@ -1,7 +1,8 @@
version: '3.8'
services: services:
server: server:
build: . build:
context: .
pull: false
restart: unless-stopped restart: unless-stopped
environment: environment:
UDP_ADDR: "0.0.0.0:9999" UDP_ADDR: "0.0.0.0:9999"
@@ -16,6 +17,7 @@ services:
dashboard: dashboard:
image: nginx:alpine image: nginx:alpine
pull_policy: if_not_present
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
+20
View File
@@ -3,6 +3,7 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"github.com/user/nanometrics/server/db" "github.com/user/nanometrics/server/db"
) )
@@ -14,7 +15,26 @@ func AgentsHandler(database *db.DB) http.HandlerFunc {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return
} }
for i := range agents {
agents[i].LastMetrics, _ = database.GetLastMetrics(agents[i].ID)
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(agents) 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 return
} }
if cfg == nil { if cfg == nil {
cfg = &models.AgentConfig{} cfg = models.DefaultAgentConfig()
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(cfg) json.NewEncoder(w).Encode(cfg)
+2
View File
@@ -3,6 +3,7 @@ package handlers
import ( import (
"bytes" "bytes"
"image" "image"
_ "image/gif"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"io" "io"
@@ -11,6 +12,7 @@ import (
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/user/nanometrics/server/db" "github.com/user/nanometrics/server/db"
_ "golang.org/x/image/webp"
) )
const maxIconSize = 128 const maxIconSize = 128
+2
View File
@@ -110,6 +110,8 @@ func main() {
handlers.IconUploadHandler(database)(w, r) handlers.IconUploadHandler(database)(w, r)
case endsWith(r.URL.Path, "/icon") && r.Method == http.MethodGet: case endsWith(r.URL.Path, "/icon") && r.Method == http.MethodGet:
handlers.IconGetHandler(database)(w, r) handlers.IconGetHandler(database)(w, r)
case r.Method == http.MethodDelete:
handlers.DeleteAgentHandler(database)(w, r)
default: default:
http.NotFound(w, r) http.NotFound(w, r)
} }
+16
View File
@@ -4,6 +4,7 @@ type AgentMetrics struct {
Hostname string `json:"hostname"` Hostname string `json:"hostname"`
IP string `json:"ip"` IP string `json:"ip"`
Status string `json:"status"` Status string `json:"status"`
Version string `json:"version"`
CPUPercent *float64 `json:"cpu_percent"` CPUPercent *float64 `json:"cpu_percent"`
MemoryUsed *int64 `json:"memory_used"` MemoryUsed *int64 `json:"memory_used"`
MemoryFree *int64 `json:"memory_free"` MemoryFree *int64 `json:"memory_free"`
@@ -32,6 +33,7 @@ type Agent struct {
IP string `json:"ip"` IP string `json:"ip"`
Status string `json:"status"` Status string `json:"status"`
LastSeen int64 `json:"last_seen"` LastSeen int64 `json:"last_seen"`
Version string `json:"version,omitempty"`
LastMetrics *AgentMetrics `json:"last_metrics,omitempty"` LastMetrics *AgentMetrics `json:"last_metrics,omitempty"`
} }
@@ -88,6 +90,20 @@ type ServerConfig struct {
PopupDetailH int `json:"popup_detail_h"` 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 { func DefaultServerConfig() ServerConfig {
return ServerConfig{ return ServerConfig{
TileMinWidth: 220, FontSize: 13, TileMinWidth: 220, FontSize: 13,
BIN
View File
Binary file not shown.
+1
View File
@@ -2,6 +2,7 @@ server {
listen 80; listen 80;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
client_max_body_size 10m;
location /api/ { location /api/ {
proxy_pass http://server:8080; proxy_pass http://server:8080;