feat(v0.1.4): SMART tile icon, IP locale robuste, copier HTTP, nettoyage UI
- Agent: détection IP via server_ip en priorité (fallback 8.8.8.8) — résout 0.0.0.0 sur LAN sans internet - Agent: détection auto des disques /sys/block (sd*, nvme*) + fix continue dans la boucle smartctl - Agent: SupplementaryGroups=disk dans le service systemd pour accès smartctl - Dashboard: icône SMART (shield-check/triangle-exclamation) dans la ligne disque de la tuile - Dashboard: bouton Copier compatible HTTP (fallback execCommand si clipboard API indisponible) - Dashboard: suppression du texte redondant dans la section INSTALLATION AGENT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
-1
@@ -248,7 +248,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nanometrics-agent"
|
||||
version = "0.1.2"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rumqttc",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nanometrics-agent"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
+12
-6
@@ -10,12 +10,18 @@ extern "C" fn handle_signal(_: libc::c_int) {
|
||||
RUNNING.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn get_local_ip() -> String {
|
||||
fn get_local_ip(server_ip: &str) -> String {
|
||||
use std::net::UdpSocket;
|
||||
if let Ok(s) = UdpSocket::bind("0.0.0.0:0") {
|
||||
if s.connect("8.8.8.8:80").is_ok() {
|
||||
if let Ok(addr) = s.local_addr() {
|
||||
return addr.ip().to_string();
|
||||
// Try server IP first (always reachable), then internet fallback
|
||||
for target in &[format!("{}:80", server_ip), "8.8.8.8:80".to_string()] {
|
||||
if let Ok(s) = UdpSocket::bind("0.0.0.0:0") {
|
||||
if s.connect(target.as_str()).is_ok() {
|
||||
if let Ok(addr) = s.local_addr() {
|
||||
let ip = addr.ip().to_string();
|
||||
if ip != "0.0.0.0" {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +43,7 @@ fn main() {
|
||||
.expect("Impossible de charger config.toml");
|
||||
|
||||
let hostname = System::host_name().unwrap_or_else(|| "unknown".to_string());
|
||||
let ip = get_local_ip();
|
||||
let ip = get_local_ip(&cfg.server.ip);
|
||||
|
||||
let mut sys = System::new();
|
||||
let mut networks = Networks::new_with_refreshed_list();
|
||||
|
||||
@@ -84,11 +84,21 @@ pub fn collect() -> Option<SmartMetrics> {
|
||||
if !is_available() {
|
||||
return None;
|
||||
}
|
||||
for dev in &["/dev/sda", "/dev/nvme0"] {
|
||||
let output = std::process::Command::new("smartctl")
|
||||
// Détecter les disques réels depuis /sys/block (sd*, nvme*)
|
||||
let mut devs: Vec<String> = std::fs::read_dir("/sys/block")
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.map(|e| e.file_name().into_string().unwrap_or_default())
|
||||
.filter(|n| n.starts_with("sd") || n.starts_with("nvme"))
|
||||
.map(|n| format!("/dev/{}", n))
|
||||
.collect();
|
||||
devs.sort();
|
||||
|
||||
for dev in &devs {
|
||||
let Ok(output) = std::process::Command::new("smartctl")
|
||||
.args(["-j", dev])
|
||||
.output()
|
||||
.ok()?;
|
||||
.output() else { continue };
|
||||
let json = String::from_utf8_lossy(&output.stdout);
|
||||
if let Ok(metrics) = parse_json(&json) {
|
||||
return Some(metrics);
|
||||
|
||||
@@ -55,6 +55,12 @@ const Grid = (() => {
|
||||
uptimeStr = d > 0 ? `${d}j ${h}h` : `${h}h`;
|
||||
}
|
||||
|
||||
const smartIco = !offline && metrics?.smart != null
|
||||
? (metrics.smart.passed
|
||||
? `<i class="fa-solid fa-shield-check" style="color:var(--ok);font-size:10px;flex-shrink:0" data-tip="SMART OK"></i>`
|
||||
: `<i class="fa-solid fa-triangle-exclamation" style="color:var(--err);font-size:10px;flex-shrink:0" data-tip="SMART FAILED"></i>`)
|
||||
: '';
|
||||
|
||||
const iconContent = `<img src="${API.iconUrl(id)}" alt=""
|
||||
style="width:100%;height:100%;object-fit:cover;border-radius:7px"
|
||||
onerror="this.style.display='none';this.nextSibling.style.display='flex'">
|
||||
@@ -87,7 +93,7 @@ const Grid = (() => {
|
||||
<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' : '')}"
|
||||
style="width:${offline ? 0 : (diskPct ?? 0).toFixed(0)}%"></div></div>
|
||||
<span class="g-val">${offline ? '—' : (metrics?.hdd_used && metrics?.hdd_total ? fmt(metrics.hdd_used) + '/' + fmt(metrics.hdd_total) : '—')}</span>
|
||||
<span class="g-val">${offline ? '—' : (metrics?.hdd_used && metrics?.hdd_total ? fmt(metrics.hdd_used) + '/' + fmt(metrics.hdd_total) : '—')}</span>${smartIco}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile-foot">
|
||||
|
||||
+23
-3
@@ -331,7 +331,6 @@ const Popups = (() => {
|
||||
<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;
|
||||
@@ -339,7 +338,7 @@ const Popups = (() => {
|
||||
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>
|
||||
onclick="Popups._copyInstallCmd(this)">Copier</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -450,10 +449,31 @@ const Popups = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
function _copyInstallCmd(btn) {
|
||||
const text = document.getElementById('s-install-cmd').value;
|
||||
const done = () => { btn.textContent = '✓ Copié'; setTimeout(() => btn.textContent = 'Copier', 1500); };
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(done).catch(() => _copyFallback(text, done));
|
||||
} else {
|
||||
_copyFallback(text, done);
|
||||
}
|
||||
}
|
||||
|
||||
function _copyFallback(text, cb) {
|
||||
const ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0';
|
||||
document.body.appendChild(ta);
|
||||
ta.focus();
|
||||
ta.select();
|
||||
try { document.execCommand('copy'); cb(); } catch (_) {}
|
||||
document.body.removeChild(ta);
|
||||
}
|
||||
|
||||
return {
|
||||
showDetail, hideDetail,
|
||||
showAgentCfg, sendAgentConfig, toggleCbox,
|
||||
showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent,
|
||||
showSmart,
|
||||
showSmart, _copyInstallCmd,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -10,6 +10,7 @@ Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
DynamicUser=yes
|
||||
SupplementaryGroups=disk
|
||||
ConfigurationDirectory=nanometrics
|
||||
ConfigurationDirectoryMode=0755
|
||||
|
||||
|
||||
Reference in New Issue
Block a user